JWT介紹
JWT的定義
Json web token (簡稱JWT),是目前最流行的跨域認證解決方案,是一種認證授權機制。
JWT 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標准。該token被設計為緊湊且安全的,特別適用於分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
JWT的由來
- HTTP協議本身是無連接、無狀態的。而這對於單純的瀏覽型網頁是十分友好的,它不需要記住是誰發送的請求,每一個請求對於它來說都是全新的,服務器也不需要額外的資源去記憶沒有個用戶。
- 但是我們現在面臨的網站大部分都需要管理不同的用戶,他們有不同的身份,例如游客、普通用戶、管理員、超級管理員等等。而這時我們就需要管理會話,需要認識每一個用戶,知道他們的具體信息。這時有人就想出了一個辦法,那就是給大家都發一個會話標識(session id),這個會話標識是一串很長的隨機字符串,每個人都不一樣, 然后每次大家發起HTTP請求的時候,把這個字符串給發送給服務器, 這樣服務器就能區分開誰是誰了。
- 不過這種方法雖然解決了基本的認證問題,但是卻給服務器帶來了巨大的負擔。每個人只需要保存自己的session id,而服務器要保存所有人的session id, 如果訪問服務器的用戶多了, 甚至需要保存成千上萬,甚至幾十萬個用戶的數據,這對服務器說是一個巨大的開銷 ,而且 嚴重的限制了服務器擴展能力。比如說我用兩個機器組成了一個集群, 用戶小明通過機器A登錄了系統, 那session id會保存在機器A上, 假設小明的下一次請求被轉發到機器B怎么辦? 機器B可沒有小明的 session id啊。這時有人提議將所有的session id保存到一個地方,讓所有的服務器到這個機器中去取數據,這樣一來,就不用復制了。但是這種方法也有很大的隱患,如果那個負責session 的機器掛了, 所有人就都得重新登錄一遍。這種情況有人嘗試把這個單點的機器也搞出集群,增加可靠性, 但不管如何, 這小小的session 對服務器來說是一個沉重的負擔
- 於是有人就思考, 我為什么要保存這可惡的session呢, 只讓每個客戶端去保存該多好。是了,我為什么要保存session——是為了確認每一個用戶的身份,並防止他們偽造。那么我可以將用戶的身份信息用簽名來保證它的不可篡改性,然后讓用戶自己保存數據。比如說我用HMAC-SHA256 算法,加上一個只有我才知道的密鑰, 對數據做一個簽名, 把這個簽名和數據一起作為token , 由於密鑰別人不知道, 就無法偽造token了。當用戶把這個token 給我發過來的時候,我再用同樣的HMAC-SHA256 算法和同樣的密鑰,對數據再計算一次簽名, 和token 中的簽名做個比較, 如果相同, 我就知道用戶已經登錄過了,並且可以直接取到用戶的user id , 如果不相同, 數據部分肯定被人篡改過, 我就告訴發送者: 對不起,沒有認證。
- 而順着這個思路,有人就想讓用戶保存更多的信息,這樣服務器的負擔就更小了。有人提出將用戶的一些其它數據和user id一起發送給用戶,讓用戶自己保存,然后利用密鑰保證安全性,這就是JWT。JWT將 Token 和 Payload 加密后存儲於客戶端,服務端只需要使用密鑰解密進行校驗(校驗也是 JWT 自己實現的)即可,不需要查詢或者減少查詢數據庫,因為 JWT 自包含了用戶信息和加密的數據。JWT的出現進一步釋放了服務器的性能。
JWT的構成
實際的 JWT 大概就像下面這樣
它是一個很長的字符串,中間用點(.)分隔成三個部分。JWT 內部是沒有換行的,這里只是為了便於展示,將它寫成了幾行。
JWT 的三個部分依次如下。
Header(頭部)
Payload(負載)
Signature(簽名)
Header
Header 部分是一個 JSON 對象,描述 JWT 的元數據,通常是下面的樣子。
{
"alg":"HS256",
"typ":"JWT"
}
jwt的頭部承載兩部分信息:
- 聲明類型:typ屬性表示這個令牌(token)的類型(type),JWT 令牌統一寫為JWT。
- 聲明加密的算法:通常直接使用 HMAC SHA256(寫成 HS256)
最后,將上面的 JSON 對象使用 Base64URL 算法轉成字符串。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Base64URL
Base64URL和Base64 算法基本類似,但有一些小的不同。
JWT 作為一個令牌(token),有些場合可能會放到 URL(比如 api.example.com/?token=xxx)。Base64 有三個字符+、/和=,在 URL 里面有特殊含義,所以要被替換掉:=被省略、+替換成-,/替換成_ 。這就是 Base64URL 算法。
Payload
載荷就是存放有效信息的地方。這個名字像是特指飛機上承載的貨品,這些有效信息包含三個部分
- 標准中注冊的聲明
- 公共的聲明
- 私有的聲明
標准中注冊的聲明 (建議但不強制使用) :
- iss: jwt簽發者
- sub: jwt所面向的用戶
- aud: 接收jwt的一方
- exp: jwt的過期時間,這個過期時間必須要大於簽發時間
- nbf: 定義在什么時間之前,該jwt都是不可用的.
- iat: jwt的簽發時間
- jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。
公共的聲明 : 公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因為該部分在客戶端可解密.
私有的聲明 : 私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因為base64是對稱解密的,意味着該部分信息可以歸類為明文信息。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后將其進行base64url加密,得到JWT的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
Signature
Signature 部分是對前兩部分的簽名,防止數據篡改。
首先,需要指定一個密鑰(secret)。這個密鑰只有服務器才知道,不能泄露給用戶。然后,使用 Header 里面指定的簽名算法(默認是 HMAC SHA256),按照下面的公式產生簽名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出簽名以后,把 Header、Payload、Signature 三個部分拼成一個字符串,每個部分之間用"點"(.)分隔,就可以返回給用戶。
JWT和Token的區別
相同點:
- 都是訪問資源的令牌
- 都可以記錄用戶的信息
- 都是使服務端無狀態化
- 都是只有驗證成功后,客戶端才能訪問服務端上受保護的資源
不同點:
-
Token:服務端驗證客戶端發送過來的 Token 時,還需要查詢數據庫獲取用戶信息,然后驗證 Token 是否有效。
-
JWT:將 Token 和 Payload 加密后存儲於客戶端,服務端只需要使用密鑰解密進行校驗(校驗也是 JWT 自己實現的)即可,不需要查詢或者減少查詢數據庫,因為 JWT 自包含了用戶信息和加密的數據。
普通token需要后端存儲與用戶的對應關系,而JWT自身攜帶對應關系。在大型的多系統中,普通token每次請求需要向用戶資源服務器獲取對應用戶信息同時驗證token,而JWT只需要驗證簽名有效即可信任且JWT自帶用戶信息, 無需額外請求。
JWT使用方式
一般是在請求頭里加入Authorization
,並加上Bearer
標注:
Authorization: Bearer + <token>
這是一種無狀態身份驗證機制,因為用戶狀態永遠不會保存在服務器內存中。 服務器受保護的路由將在授權頭中檢查有效的JWT,如果存在,則允許用戶訪問受保護的資源。 由於JWT是獨立的,所有必要的信息都在那里,減少了多次查詢數據庫的需求。
這使得我們可以完全依賴無狀態的數據API,甚至向下游服務提出請求。 無論哪些域正在為API提供服務並不重要,因此不會出現跨域資源共享(CORS)的問題,因為它不使用Cookie
JWT的認證流程:
- 用戶輸入用戶名/密碼登錄,服務端認證成功后,會返回給客戶端一個 JWT
- 客戶端將 token 保存到本地(通常使用 localstorage,也可以使用 cookie)
- 當用戶希望訪問一個受保護的路由或者資源的時候,需要請求頭的 Authorization 字段中使用Bearer 模式添加 JWT。
- 服務端的保護路由將會檢查請求頭 Authorization 中的 JWT 信息,如果合法,則允許用戶的行為
JWT的安全風險
1.敏感信息泄露
我們能夠輕松解碼payload和header,因為這兩個都只經過Base64Url編碼,而有的時候開發者會誤將敏感信息存在payload中。
一般我們遇到jwt字符串可以到https://jwt.io/這個網站解密。
2.未校驗簽名
某些服務端並未校驗JWT簽名,所以,可以嘗試修改signature后(或者直接刪除signature)看其是否還有效。
3.簽名算法可被修改為none(CVE-2015-2951)
頭部用來聲明token的類型和簽名用的算法等,比如:
{
"alg": "HS256",
"typ": "JWT"
}
以上header指定了簽名算法為HS256,意味着服務端利用此算法將header和payload進行加密,形成signature,同時接收到token時,也會利用此算法對signature進行簽名驗證。
但是如果我們修改了簽名算法會怎么樣?比如將header修改為:
{
"alg": "none",
"typ": "JWT"
}
那么服務端接收到token后會將其認定為無加密算法, 於是對signature的檢驗也就失效了,那么我們就可以隨意修改payload部分偽造token。
https://jwt.io將alg為none視為惡意行為,所以,無法通過在線工具生成JWT,可以用python的jwt庫來實現:
import jwt
jwt.encode({"user":"admin","action":"upload"},algorithm="none",key="")
用none算法生成的JWT只有兩部分了,根本連簽名都不存在。
4.簽名密鑰可被爆破
jwt使用算法對header和payload進行加密,如果我們可以爆破出加密密鑰,那么也就可以隨意修改token了。
JWT爆破腳本:https://github.com/Ch1ngg/JWTPyCrack
也可以使用下面的腳本爆破
jwt_str = "xxx.ttt.zzz"
path = "D:/keys.txt"
alg = "HS256"
with open(path,encoding='utf-8') as f:
for line in f:
key_ = line.strip()
try:
jwt.decode(jwt_str,verify=True,key=key_,algorithm=alg)
print('found key! --> ' + key_)
break
except(jwt.exceptions.ExpiredSignatureError, jwt.exceptions.InvalidAudienceError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.ImmatureSignatureError):
print('found key! --> ' + key_)
break
except(jwt.exceptions.InvalidSignatureError):
continue
else:
print("key not found!")
5.修改非對稱密碼算法為對稱密碼算法(CVE-2016-10555)
JWT的簽名加密算法有兩種,對稱加密算法和非對稱加密算法。
對稱加密算法比如HS256,加解密使用同一個密鑰,保存在后端。
非對稱加密算法比如RS256,后端加密使用私鑰,前端解密使用公鑰,公鑰是我們可以獲取到的。
如果我們修改header,將算法從RS256更改為HS256,后端代碼會使用RS256的公鑰作為HS256算法的密鑰。於是我們就可以用RS256的公鑰偽造數據
比如說這道CTF題目:https://skysec.top/2018/05/19/2
6.偽造密鑰(CVE-2018-0114)
jwk是header里的一個參數,用於指出密鑰,存在被偽造的風險。比如CVE-2018-0114: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-0114
攻擊者可以通過以下方法來偽造JWT:刪除原始簽名,向標頭添加新的公鑰,然后使用與該公鑰關聯的私鑰進行簽名。
比如:
{
"typ": "JWT",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"kid": "TEST",
"use": "sig",
"e": "AQAB",
"n": "oUGnPChFQAN1xdA1_f_FWZdFAis64o5hdVyFm4vVFBzTIEdYmZZ3hJHsWi5b_m_tjsgjhCZZnPOLn-ZVYs7pce__rDsRw9gfKGCVzvGYvPY1hkIENNeBfSaQlBhOhaRxA85rBkg8BX7zfMRQJ0fMG3EAZhYbr3LDtygwSXi66CCk4zfFNQfOQEF-Tgv1kgdTFJW-r3AKSQayER8kF3xfMuI7-VkKz-yyLDZgITyW2VWmjsvdQTvQflapS1_k9IeTjzxuKCMvAl8v_TFj2bnU5bDJBEhqisdb2BRHMgzzEBX43jc-IHZGSHY2KA39Tr42DVv7gS--2tyh8JluonjpdQ"
}
}
JWT tool
此工具可用於測試jwt的安全性,地址是 https://github.com/ticarpi/jwt_tool
示例用法:
JWT的使用建議
- 不要存放敏感信息在Token里。
- Payload中的exp時效不要設定太長。
- 開啟Only Http預防XSS攻擊。
- 如果擔心重播攻擊(replay attacks )可以增加jti(JWT ID),exp(有效時間) Claim。
- 在你的應用程序應用層中增加黑名單機制,必要的時候可以進行Block做阻擋(這是針對掉令牌被第三方使用竊取的手動防御)。