前言
雖然前兩年系統(tǒng)中就已經(jīng)應(yīng)用了JWT作為token用來鑒權(quán)認(rèn)證等,大概知道那么回事雷激,但是經(jīng)常交流過程中袜匿,面對別人的疑問沒法給一個深入的解答,所以重新梳理了下JWT相關(guān)知識诫给。
1.什么是JWT?
JWT官網(wǎng)
JSON Web Token (JWT)是一個開放標(biāo)準(zhǔn)(RFC 7519)香拉,它定義了一種緊湊且自包含的方式啦扬,以JSON對象的方式在各方之間安全地傳輸信息,因?yàn)槭腔跀?shù)字簽名所以可以進(jìn)行驗(yàn)證和信任凫碌,常用的算法比如有HMAC扑毡、RSA等等。
2.JWT數(shù)據(jù)結(jié)構(gòu)
JWT由三部分組成,以"點(diǎn)"(.)作為分隔符盛险,看起來格式這樣的xxx.yyy.zzz
接下來看一下這三部分的具體構(gòu)成:
- Header
Header是JWT的第一部分费什,包含算法類型和token類型的說明方援,舉例如下,算法是用的HS256指的是HMACSHA256,token類型是JWT言沐。
HMACSHA256不了解?可以看看對算法介紹的這一篇密碼學(xué)相關(guān)知識梳理
Base64Url不了解作煌?Base64和Base64Url區(qū)別是什么清蚀?下文會對Base64和Base64URL做一些簡單介紹。
例子:Header內(nèi)容
{
"alg": "HS256",
"typ": "JWT"
}
對該json進(jìn)行Base64Url編碼得到
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9,也就是xxx.yyy.zzz的xxx部分的值。
- Payload:載荷
Payload是JWT第二部分祟牲,我們把數(shù)據(jù)放在這部分,舉例如下
例子:Payload內(nèi)容
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
對該json進(jìn)行Base64Url編碼得到
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
- Signature:簽名
Signature是JWT第三部分隙畜,是對前兩塊內(nèi)容的簽名值,比如以簽名算法HMACSHA256為例
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
按照上面計(jì)算規(guī)則:
base64UrlEncode(header) + "."+base64UrlEncode(payload)=xxx.yyy
也就是說第三部分的值就是對“xxx.yyy”進(jìn)行HMACSHA256摘要計(jì)算,這里secret假設(shè)等于“123456789”
計(jì)算出來的第三部分值:S2ZL7D-D3VeduQ44Cy2qLRFxHV43gRGSZtlfJ2MJ57g
最后把上面三部分拼接到一起組成完整的JWT如下说贝,從上面的計(jì)算規(guī)則我們可以發(fā)現(xiàn)议惰,JWT的安全性其實(shí)就是基于算法的原理,比如用的HMACSHA256乡恕,那么密鑰就是安全的保證言询,別人拿不到密鑰就無法偽造,無法篡改JWT傲宜。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.S2ZL7D-D3VeduQ44Cy2qLRFxHV43gRGSZtlfJ2MJ57g
2.1一些理解:
- Q1:Payload是否可以存敏感數(shù)據(jù)倍试?
- A1:不能,Header和Payload都只是對數(shù)據(jù)進(jìn)行Base64Url編碼而已蛋哭,了解Base64的話县习,應(yīng)該知道這個不是加密,第三方拿到編碼數(shù)據(jù)可以直接解碼谆趾,所以不要放任何敏感數(shù)據(jù)躁愿,除非額外對數(shù)據(jù)又進(jìn)行加密處理。
- Q2:第三方是否可以偽造JWT沪蓬?是否可以篡改Payload里面的數(shù)據(jù)彤钟?
- A2:從上面第三部分Signature簽名值計(jì)算規(guī)則,我們可以看到跷叉,是用HMACSHA256算法以及密鑰key計(jì)算Header和Payload的摘要值逸雹,密鑰key是服務(wù)端私有的,理論上攻擊者沒有密鑰key就無法生成出一樣的簽名值云挟,所以結(jié)論是無法偽造jwt梆砸,同樣的篡改Payload里面的數(shù)據(jù),服務(wù)端計(jì)算出來的簽名值就會不匹配园欣,這樣就可以認(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通過計(jì)算窃诉,結(jié)果A是HMACSHA256計(jì)算的摘要值字節(jié)數(shù)組轉(zhuǎn)十六制的值64個字符(sha256計(jì)算摘要值是256bit,1個字節(jié)8bit,256bit對應(yīng)32個字節(jié)褐奴,1個字節(jié)用2位16進(jìn)制表示按脚,所以結(jié)果A是64位),然后對結(jié)果A進(jìn)行Base編碼得到結(jié)果A' 88個字符(64位十六進(jìn)制讀取的時候每一位當(dāng)做一個字符讀榷囟),結(jié)果B是對HMACSHA256計(jì)算出來的32個字節(jié)原始二進(jìn)制直接進(jìn)行Base64編碼得到的是44個字符辅搬,結(jié)果B是才是正確的流程,結(jié)果A為什么會長度多一倍呢脖旱?因?yàn)楹灻?2個字節(jié)轉(zhuǎn)成16進(jìn)制后堪遂,在計(jì)算Base64的讀取十六進(jìn)制的時候是以字符讀取,變成了64個字節(jié)萌庆,一出一進(jìn)長度翻了一倍溶褪。
3.Base64和Base64Url
3.1Base64算法
Base64主要是對給定的字符和字符編碼(如ASCII碼,UTF-8碼)對應(yīng)的十進(jìn)制數(shù)為基準(zhǔn)践险,做編碼操作:
1.將給定的字符串以字符為單位轉(zhuǎn)換為對應(yīng)的字符編碼(如ASCII碼)
2.將獲得的字符編碼轉(zhuǎn)換為二進(jìn)制碼
3.對獲得的二進(jìn)制碼做分組轉(zhuǎn)換操作猿妈,每3個8位二進(jìn)制碼為一組,轉(zhuǎn)換為每4個6位二進(jìn)制碼為一組(不足6位時地位補(bǔ)0)巍虫,這是一個分組變化的過程彭则,3個8位二進(jìn)制碼和4個6位二進(jìn)制碼的長度都是24位(38=46=24)
4.對獲得的4個6位二進(jìn)制碼補(bǔ)位,向6位二進(jìn)制碼添加2位高位0占遥,組成4個8位二進(jìn)制碼
5.將獲得的4個8位二進(jìn)制碼轉(zhuǎn)換為十進(jìn)制碼
6.將獲得的十進(jìn)制碼轉(zhuǎn)換為Base64字符表中對應(yīng)的字符
舉例如下:ASCII碼字符編碼
對字符串“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)過Base64編碼后得到"QQ=="這樣一個字符串,Base64是以4個字符為單位瓦胎,其長度只能是4個字符的整數(shù)倍芬萍,不滿足時就用等號補(bǔ)位。
3.2 Base64Url
查看Base64字符映射表我們可以知道有一些字符比如"/"搔啊、“+”柬祠、“=”,像這種字符在Url中有特殊的意義坯癣,所以我們需要替代這些字符瓶盛,替代規(guī)則是“-”替代“+”,"_"替代“/”,"="直接去掉最欠,這就是Base64Url編碼方式示罗。
- Base64Url編碼的流程
1.明文使用BASE64進(jìn)行編碼
2. 在BASE64的基礎(chǔ)上進(jìn)行以下的編碼:
去除尾部的"="
把"+"替換成"-"
把"/"替換成"_"
- Base64Url解碼的流程
1.替換字符
把"-"替換成"+"
把"_"替換成"/"
(計(jì)算Base64Url編碼長度)%4
結(jié)果為0,不做處理
結(jié)果為2芝硬,字符串末尾添加"=="
結(jié)果為3蚜点,字符串末尾添加"="
2、使用Base64解碼密文拌阴,得到原始的明文
3.2.1Base64Url解碼問題
交流的時候有個同事剛好提出為什么補(bǔ)等號的時候绍绘, (計(jì)算Base64Url編碼長度)%4結(jié)果為0,2,3陪拘,沒有為1的情況厂镇?
接下來看一下這個計(jì)算過程,前面提到Base64編碼規(guī)則每3個8位二進(jìn)制碼為一組左刽,轉(zhuǎn)換為每4個6位二進(jìn)制碼為一組(不足6位時地位補(bǔ)0)捺信,假設(shè)我們對一組字節(jié)編碼,比如22個字節(jié)欠痴,每三個字節(jié)一組迄靠,最后剩下一個字節(jié)(22mod3=1),23個字節(jié)分組后剩下2個字節(jié)(23mod3=2),21個字節(jié)則正好分成7組喇辽,所以分組過程中只可能剩1個字節(jié)或者2個字節(jié)掌挚,下面對剩1個和2個字節(jié)的分析
- 1個字節(jié)
從前面對字符"A"編碼過程也可以看到,如果一個字節(jié)比如01000001菩咨,拆成4-6二進(jìn)制的時候是010000和010000(本來是拆成6位010000和01吠式,不足6位的補(bǔ)0),4-8二進(jìn)制變成00010000和00010000抽米,然后在轉(zhuǎn)10進(jìn)制然后從Base64映射對應(yīng)的字符得到兩個字符QQ奇徒,Base64是4個字符位單位,所以補(bǔ)兩個"=="缨硝。 - 2個字節(jié)
從上面剩1個字節(jié)的分析可以很容易的看出摩钙,2個字節(jié)拆成4-6二進(jìn)制的時候?qū)?yīng)3個4-6,4-8二進(jìn)制也是3個查辩,最后得到字符3個胖笛,補(bǔ)一個“=”號。
所以轉(zhuǎn)成Base64后宜岛,可能剛好分組长踊,或補(bǔ)1個等號或2個等號這三種,不存在補(bǔ)3個等號的萍倡。
3.3擴(kuò)展的一些思考
我們將JWT用作系統(tǒng)token機(jī)制身弊,用戶登錄后服務(wù)端頒發(fā)token,后續(xù)請求頭帶上token列敲,服務(wù)端檢驗(yàn)該token是否合法來判斷用戶的合法性阱佛。
- 對于現(xiàn)在很多單頁面應(yīng)用比如基于AngluarJS,Vue戴而,拿到token后存在了localstorage中凑术,后續(xù)請求在從localstorage中取出,這樣是否安全所意?是否有更好的方案淮逊?
- 我們在以前pc時代通過session催首,cookie機(jī)制進(jìn)行會話跟蹤,那這兩種機(jī)制本質(zhì)上的差別是什么泄鹏?
這兩個問題因?yàn)檫€沒有完全想明白也沒有一個完美的解決方案郎任,所以暫時拋出來記錄一下。
4.附錄
-
Base64索引表
- 參考文章
阮一峰寫的JSON Web Token 入門教程
Base64的介紹以及Base64URL介紹
書籍《Java加密與解密的藝術(shù)》