前言
雖然前兩年系統(tǒng)中就已經(jīng)應(yīng)用了JWT作為token用來(lái)鑒權(quán)認(rèn)證等,大概知道那么回事,但是經(jīng)常交流過(guò)程中,面對(duì)別人的疑問(wèn)沒(méi)法給一個(gè)深入的解答,所以重新梳理了下JWT相關(guān)知識(shí)。
1.什么是JWT?
JWT官網(wǎng)
JSON Web Token (JWT)是一個(gè)開(kāi)放標(biāo)準(zhǔn)(RFC 7519),它定義了一種緊湊且自包含的方式,以JSON對(duì)象的方式在各方之間安全地傳輸信息,因?yàn)槭腔跀?shù)字簽名所以可以進(jìn)行驗(yàn)證和信任,常用的算法比如有HMAC、RSA等等。
2.JWT數(shù)據(jù)結(jié)構(gòu)
JWT由三部分組成,以"點(diǎn)"(.)作為分隔符,看起來(lái)格式這樣的xxx.yyy.zzz
接下來(lái)看一下這三部分的具體構(gòu)成:
- Header
Header是JWT的第一部分,包含算法類(lèi)型和token類(lèi)型的說(shuō)明,舉例如下,算法是用的HS256指的是HMACSHA256,token類(lèi)型是JWT。
HMACSHA256不了解?可以看看對(duì)算法介紹的這一篇密碼學(xué)相關(guān)知識(shí)梳理
Base64Url不了解?Base64和Base64Url區(qū)別是什么?下文會(huì)對(duì)Base64和Base64URL做一些簡(jiǎn)單介紹。
例子:Header內(nèi)容
{
"alg": "HS256",
"typ": "JWT"
}
對(duì)該json進(jìn)行Base64Url編碼得到
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9,也就是xxx.yyy.zzz的xxx部分的值。
- Payload:載荷
Payload是JWT第二部分,我們把數(shù)據(jù)放在這部分,舉例如下
例子:Payload內(nèi)容
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
對(duì)該json進(jìn)行Base64Url編碼得到
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
- Signature:簽名
Signature是JWT第三部分,是對(duì)前兩塊內(nèi)容的簽名值,比如以簽名算法HMACSHA256為例
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
按照上面計(jì)算規(guī)則:
base64UrlEncode(header) + "."+base64UrlEncode(payload)=xxx.yyy
也就是說(shuō)第三部分的值就是對(duì)“xxx.yyy”進(jìn)行HMACSHA256摘要計(jì)算,這里secret假設(shè)等于“123456789”
計(jì)算出來(lái)的第三部分值:S2ZL7D-D3VeduQ44Cy2qLRFxHV43gRGSZtlfJ2MJ57g
最后把上面三部分拼接到一起組成完整的JWT如下,從上面的計(jì)算規(guī)則我們可以發(fā)現(xiàn),JWT的安全性其實(shí)就是基于算法的原理,比如用的HMACSHA256,那么密鑰就是安全的保證,別人拿不到密鑰就無(wú)法偽造,無(wú)法篡改JWT。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.S2ZL7D-D3VeduQ44Cy2qLRFxHV43gRGSZtlfJ2MJ57g
2.1一些理解:
- Q1:Payload是否可以存敏感數(shù)據(jù)?
- A1:不能,Header和Payload都只是對(duì)數(shù)據(jù)進(jìn)行Base64Url編碼而已,了解Base64的話,應(yīng)該知道這個(gè)不是加密,第三方拿到編碼數(shù)據(jù)可以直接解碼,所以不要放任何敏感數(shù)據(jù),除非額外對(duì)數(shù)據(jù)又進(jìn)行加密處理。
- Q2:第三方是否可以偽造JWT?是否可以篡改Payload里面的數(shù)據(jù)?
- A2:從上面第三部分Signature簽名值計(jì)算規(guī)則,我們可以看到,是用HMACSHA256算法以及密鑰key計(jì)算Header和Payload的摘要值,密鑰key是服務(wù)端私有的,理論上攻擊者沒(méi)有密鑰key就無(wú)法生成出一樣的簽名值,所以結(jié)論是無(wú)法偽造jwt,同樣的篡改Payload里面的數(shù)據(jù),服務(wù)端計(jì)算出來(lái)的簽名值就會(huì)不匹配,這樣就可以認(rèn)為被篡改了,直接拒絕服務(wù),所以密鑰key是關(guān)鍵,不能泄露。
- Q3:假設(shè)如下截圖中消息是xxx.yyy的值,HMACSHA256簽名后在進(jìn)行Base64Url編碼得到的是如下截圖中的結(jié)果A'還是結(jié)果B?
- A3:答案是B,詳細(xì)解釋如下:
如下截圖中消息是base64UrlEncode(header) + "." +base64UrlEncode(payload),也就是待簽名內(nèi)容,使用密鑰key通過(guò)計(jì)算,結(jié)果A是HMACSHA256計(jì)算的摘要值字節(jié)數(shù)組轉(zhuǎn)十六制的值64個(gè)字符(sha256計(jì)算摘要值是256bit,1個(gè)字節(jié)8bit,256bit對(duì)應(yīng)32個(gè)字節(jié),1個(gè)字節(jié)用2位16進(jìn)制表示,所以結(jié)果A是64位),然后對(duì)結(jié)果A進(jìn)行Base編碼得到結(jié)果A' 88個(gè)字符(64位十六進(jìn)制讀取的時(shí)候每一位當(dāng)做一個(gè)字符讀?。?結(jié)果B是對(duì)HMACSHA256計(jì)算出來(lái)的32個(gè)字節(jié)原始二進(jìn)制直接進(jìn)行Base64編碼得到的是44個(gè)字符,結(jié)果B是才是正確的流程,結(jié)果A為什么會(huì)長(zhǎng)度多一倍呢?因?yàn)楹灻?2個(gè)字節(jié)轉(zhuǎn)成16進(jìn)制后,在計(jì)算Base64的讀取十六進(jìn)制的時(shí)候是以字符讀取,變成了64個(gè)字節(jié),一出一進(jìn)長(zhǎng)度翻了一倍。

3.Base64和Base64Url
3.1Base64算法
Base64主要是對(duì)給定的字符和字符編碼(如ASCII碼,UTF-8碼)對(duì)應(yīng)的十進(jìn)制數(shù)為基準(zhǔn),做編碼操作:
1.將給定的字符串以字符為單位轉(zhuǎn)換為對(duì)應(yīng)的字符編碼(如ASCII碼)
2.將獲得的字符編碼轉(zhuǎn)換為二進(jìn)制碼
3.對(duì)獲得的二進(jìn)制碼做分組轉(zhuǎn)換操作,每3個(gè)8位二進(jìn)制碼為一組,轉(zhuǎn)換為每4個(gè)6位二進(jìn)制碼為一組(不足6位時(shí)地位補(bǔ)0),這是一個(gè)分組變化的過(guò)程,3個(gè)8位二進(jìn)制碼和4個(gè)6位二進(jìn)制碼的長(zhǎng)度都是24位(38=46=24)
4.對(duì)獲得的4個(gè)6位二進(jìn)制碼補(bǔ)位,向6位二進(jìn)制碼添加2位高位0,組成4個(gè)8位二進(jìn)制碼
5.將獲得的4個(gè)8位二進(jìn)制碼轉(zhuǎn)換為十進(jìn)制碼
6.將獲得的十進(jìn)制碼轉(zhuǎn)換為Base64字符表中對(duì)應(yīng)的字符
舉例如下:ASCII碼字符編碼
對(duì)字符串“A”進(jìn)行Base64編碼,如下表示:
字符 A
ASCII碼 65
二進(jìn)制碼 01000001
4-6二進(jìn)制碼 010000 010000
4-8二進(jìn)制碼 00010000 00010000
十進(jìn)制碼 16 16
字符表映射碼 Q Q = =
字符“A‘經(jīng)過(guò)Base64編碼后得到"QQ=="這樣一個(gè)字符串,Base64是以4個(gè)字符為單位,其長(zhǎng)度只能是4個(gè)字符的整數(shù)倍,不滿足時(shí)就用等號(hào)補(bǔ)位。
3.2 Base64Url
查看Base64字符映射表我們可以知道有一些字符比如"/"、“+”、“=”,像這種字符在Url中有特殊的意義,所以我們需要替代這些字符,替代規(guī)則是“-”替代“+”,"_"替代“/”,"="直接去掉,這就是Base64Url編碼方式。
- Base64Url編碼的流程
1.明文使用BASE64進(jìn)行編碼
2. 在BASE64的基礎(chǔ)上進(jìn)行以下的編碼:
去除尾部的"="
把"+"替換成"-"
把"/"替換成"_"
- Base64Url解碼的流程
1.替換字符
把"-"替換成"+"
把"_"替換成"/"
(計(jì)算Base64Url編碼長(zhǎng)度)%4
結(jié)果為0,不做處理
結(jié)果為2,字符串末尾添加"=="
結(jié)果為3,字符串末尾添加"="
2、使用Base64解碼密文,得到原始的明文
3.2.1Base64Url解碼問(wèn)題
交流的時(shí)候有個(gè)同事剛好提出為什么補(bǔ)等號(hào)的時(shí)候, (計(jì)算Base64Url編碼長(zhǎng)度)%4結(jié)果為0,2,3,沒(méi)有為1的情況?
接下來(lái)看一下這個(gè)計(jì)算過(guò)程,前面提到Base64編碼規(guī)則每3個(gè)8位二進(jìn)制碼為一組,轉(zhuǎn)換為每4個(gè)6位二進(jìn)制碼為一組(不足6位時(shí)地位補(bǔ)0),假設(shè)我們對(duì)一組字節(jié)編碼,比如22個(gè)字節(jié),每三個(gè)字節(jié)一組,最后剩下一個(gè)字節(jié)(22mod3=1),23個(gè)字節(jié)分組后剩下2個(gè)字節(jié)(23mod3=2),21個(gè)字節(jié)則正好分成7組,所以分組過(guò)程中只可能剩1個(gè)字節(jié)或者2個(gè)字節(jié),下面對(duì)剩1個(gè)和2個(gè)字節(jié)的分析
- 1個(gè)字節(jié)
從前面對(duì)字符"A"編碼過(guò)程也可以看到,如果一個(gè)字節(jié)比如01000001,拆成4-6二進(jìn)制的時(shí)候是010000和010000(本來(lái)是拆成6位010000和01,不足6位的補(bǔ)0),4-8二進(jìn)制變成00010000和00010000,然后在轉(zhuǎn)10進(jìn)制然后從Base64映射對(duì)應(yīng)的字符得到兩個(gè)字符QQ,Base64是4個(gè)字符位單位,所以補(bǔ)兩個(gè)"=="。 - 2個(gè)字節(jié)
從上面剩1個(gè)字節(jié)的分析可以很容易的看出,2個(gè)字節(jié)拆成4-6二進(jìn)制的時(shí)候?qū)?yīng)3個(gè)4-6,4-8二進(jìn)制也是3個(gè),最后得到字符3個(gè),補(bǔ)一個(gè)“=”號(hào)。
所以轉(zhuǎn)成Base64后,可能剛好分組,或補(bǔ)1個(gè)等號(hào)或2個(gè)等號(hào)這三種,不存在補(bǔ)3個(gè)等號(hào)的。
3.3擴(kuò)展的一些思考
我們將JWT用作系統(tǒng)token機(jī)制,用戶(hù)登錄后服務(wù)端頒發(fā)token,后續(xù)請(qǐng)求頭帶上token,服務(wù)端檢驗(yàn)該token是否合法來(lái)判斷用戶(hù)的合法性。
- 對(duì)于現(xiàn)在很多單頁(yè)面應(yīng)用比如基于AngluarJS,Vue,拿到token后存在了localstorage中,后續(xù)請(qǐng)求在從localstorage中取出,這樣是否安全?是否有更好的方案?
- 我們?cè)谝郧皃c時(shí)代通過(guò)session,cookie機(jī)制進(jìn)行會(huì)話跟蹤,那這兩種機(jī)制本質(zhì)上的差別是什么?
這兩個(gè)問(wèn)題因?yàn)檫€沒(méi)有完全想明白也沒(méi)有一個(gè)完美的解決方案,所以暫時(shí)拋出來(lái)記錄一下。
4.附錄
-
Base64索引表
- 參考文章
阮一峰寫(xiě)的JSON Web Token 入門(mén)教程
Base64的介紹以及Base64URL介紹
書(shū)籍《Java加密與解密的藝術(shù)》
