什么是JWT
根據(jù)維基百科的定義,JSON WEB Token(JWT,讀作 [/d??t/]),是一種基于JSON的、用于在網(wǎng)絡(luò)上聲明某種主張的令牌(token)。JWT通常由三部分組成: 頭信息(header), 消息體(payload)和簽名(signature)。
頭信息指定了該JWT使用的簽名算法:
HS256 表示使用了 HMAC-SHA256 來生成簽名。
消息體包含了JWT的意圖:
payload = '{'loggedInAs':'admin','iat':1422779638}'//iat表示令牌生成的時間
未簽名的令牌由base64url編碼的頭信息和消息體拼接而成(使用”.”分隔),簽名則通過私有的key計算而成:
最后在未簽名的令牌尾部拼接上base64url編碼的簽名(同樣使用”.”分隔)就是JWT了:
token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature) # token看起來像這樣: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI
JWT常常被用作保護服務(wù)端的資源(resource),客戶端通常將JWT通過HTTP的Authorizationheader發(fā)送給服務(wù)端,服務(wù)端使用自己保存的key計算、驗證簽名以判斷該JWT是否可信:
那怎么就誤用了呢
近年來RESTful API開始風(fēng)靡,使用HTTP header來傳遞認(rèn)證令牌似乎變得理所應(yīng)當(dāng),而單頁應(yīng)用(SPA)、前后端分離架構(gòu)似乎正在促成越來越多的WEB應(yīng)用放棄歷史悠久的cookie-session認(rèn)證機制,轉(zhuǎn)而使用JWT來管理用戶session。支持該方案的人認(rèn)為:
1.該方案更易于水平擴展
在cookie-session方案中,cookie內(nèi)僅包含一個session標(biāo)識符,而諸如用戶信息、授權(quán)列表等都保存在服務(wù)端的session中。如果把session中的認(rèn)證信息都保存在JWT中,在服務(wù)端就沒有session存在的必要了。當(dāng)服務(wù)端水平擴展的時候,就不用處理session復(fù)制(session replication)/ session黏連(sticky session)或是引入外部session存儲了。
從這個角度來說,這個優(yōu)點確實存在,但實際上外部session存儲方案已經(jīng)非常成熟了(比如Redis),在一些Framework的幫助下(比如spring-session和hazelcast),session復(fù)制也并沒有想象中的麻煩。所以除非你的應(yīng)用訪問量非常非常非常(此處省略N個非常)大,使用cookie-session配合外部session存儲完全夠用了。
2.該方案可防護CSRF攻擊
跨站請求偽造Cross-site request forgery(簡稱CSRF, 讀作 [sea-surf])是一種典型的利用cookie-session漏洞的攻擊,這里借用spring-security的一個例子來解釋CSRF:
假設(shè)你經(jīng)常使用bank.example.com進行網(wǎng)上轉(zhuǎn)賬,在你提交轉(zhuǎn)賬請求時bank.example.com的前端代碼會提交一個HTTP請求:
POST /transfer HTTP/1.1Host: bank.example.comcookie: JsessionID=randomid; Domain=bank.example.com; Secure; HttpOnlyContent-Type: application/x-www-form-urlencodedamount=100.00&routingNumber=1234&account=9876
你圖方便沒有登出bank.example.com,隨后又訪問了一個惡意網(wǎng)站,該網(wǎng)站的HTML頁面包含了這樣一個表單:
你被“點擊就送”吸引了,當(dāng)你點了提交按鈕時你已經(jīng)向攻擊者的賬號轉(zhuǎn)了100元?,F(xiàn)實中的攻擊可能更隱蔽,惡意網(wǎng)站的頁面可能使用Javascript自動完成提交。盡管惡意網(wǎng)站沒有辦法盜取你的session cookie(從而假冒你的身份),但惡意網(wǎng)站向bank.example.com發(fā)起請求時,你的cookie會被自動發(fā)送過去。
因此,有些人認(rèn)為前端代碼將JWT通過HTTP header發(fā)送給服務(wù)端(而不是通過cookie自動發(fā)送)可以有效防護CSRF。在這種方案中,服務(wù)端代碼在完成認(rèn)證后,會在HTTP response的header中返回JWT,前端代碼將該JWT存放到Local Storage里待用,或是服務(wù)端直接在cookie中保存HttpOnly=false的JWT。
在向服務(wù)端發(fā)起請求時,用Javascript取出JWT(否則前端Javascript代碼無權(quán)從cookie中獲取數(shù)據(jù)),再通過header發(fā)送回服務(wù)端通過認(rèn)證。由于惡意網(wǎng)站的代碼無法獲取bank.example.com的cookie/Local Storage中的JWT,這種方式確實能防護CSRF,但將JWT保存在cookie/Local Storage中可能會給另一種攻擊可乘之機,我們一會詳細(xì)討論它:跨站腳本攻擊——XSS。
3.該方案更安全
由于JWT要求有一個秘鑰,還有一個算法,生成的令牌看上去不可讀,不少人誤認(rèn)為該令牌是被加密的。但實際上秘鑰和算法是用來生成簽名的,令牌本身不可讀僅是因為base64url編碼,可以直接解碼,所以如果JWT中如果保存了敏感的信息,相對cookie-session將數(shù)據(jù)放在服務(wù)端來說,更不安全。
除了以上這些誤解外,使用JWT管理session還有如下缺點:
看到這里后,你可能發(fā)現(xiàn),將JWT保存在Local Storage中,并使用JWT來管理session并不是一個好主意,那有沒有可能“正確”地使用JWT來管理session呢?比如:
這個方案看上去是挺不錯的,恭喜你,你重新發(fā)明了cookie-session,可能實現(xiàn)還不一定有現(xiàn)有的好。
那究竟JWT可以用來做什么
我的同事做過一個形象的解釋:
JWT(其實還有SAML)最適合的應(yīng)用場景就是“開票”,或者“簽字”。
在有紙化辦公時代,多部門、多組織之間的協(xié)同工作往往會需要拿著A部門領(lǐng)導(dǎo)的“簽字”或者“蓋章”去B部門“使用”或者“訪問”對應(yīng)的資源,其實這種“領(lǐng)導(dǎo)簽字/蓋章”就是JWT,都是一種由具有一定權(quán)力的實體“簽發(fā)”并“授權(quán)”的“票據(jù)”。一般的,這種票據(jù)具有可驗證性(領(lǐng)導(dǎo)簽名/蓋章可以被驗證,且難于模仿),不可篡改性(涂改過的文件不被接受,除非在涂改處再次簽字確認(rèn));并且這種票據(jù)一般都是“一次性”使用的,在訪問到對應(yīng)的資源后,該票據(jù)一般會被資源持有方收回留底,用于后續(xù)的審計、追溯等用途。
舉兩個例子:
在以上的兩個例子中,“請假申請單”和“用車申請單”就是JWT中的payload,領(lǐng)導(dǎo)簽字就是base64后的數(shù)字簽名,領(lǐng)導(dǎo)是issuer,“HR部門的韓梅梅”和“司機老王”即為JWT的audience,audience需要驗證領(lǐng)導(dǎo)簽名是否合法,驗證合法后根據(jù)payload中請求的資源給予相應(yīng)的權(quán)限,同時將JWT收回。
放到系統(tǒng)集成的場景中,JWT更適合一次性操作的認(rèn)證:
服務(wù)B你好, 服務(wù)A告訴我,我可以操作<JWT內(nèi)容>, 這是我的憑證(即JWT)
在這里,服務(wù)A負(fù)責(zé)認(rèn)證用戶身份(相當(dāng)于上例中領(lǐng)導(dǎo)批準(zhǔn)請假),并頒布一個很短過期時間的JWT給瀏覽器(相當(dāng)于上例中的請假單),瀏覽器(相當(dāng)于上例中的請假員工)在向服務(wù)B的請求中帶上該JWT,則服務(wù)B(相當(dāng)于上例中的HR員工)可以通過驗證該JWT來判斷用戶是否有權(quán)執(zhí)行該操作。這樣,服務(wù)B就成為一個安全的無狀態(tài)的服務(wù)了。
總結(jié)