1. JSON Web Token是什么
JSON Web Token (JWT)是一個開放標准(RFC 7519),它定義了一種緊湊的、自包含的方式,用於作為JSON對象在各方之間安全地傳輸信息。該信息可以被驗證和信任,因為它是數字簽名的。
2. 什么時候你應該用JSON Web Tokens
下列場景中使用JSON Web Token是很有用的:
- Authorization (授權) : 這是使用JWT的最常見場景。一旦用戶登錄,后續每個請求都將包含JWT,允許用戶訪問該令牌允許的路由、服務和資源。單點登錄是現在廣泛使用的JWT的一個特性,因為它的開銷很小,並且可以輕松地跨域使用。
- Information Exchange (信息交換) : 對於安全的在各方之間傳輸信息而言,JSON Web Tokens無疑是一種很好的方式。因為JWTs可以被簽名,例如,用公鑰/私鑰對,你可以確定發送人就是它們所說的那個人。另外,由於簽名是使用頭和有效負載計算的,您還可以驗證內容沒有被篡改。
3. JSON Web Token的結構是什么樣的
JSON Web Token由三部分組成,它們之間用圓點(.)連接。這三部分分別是:
- Header
- Payload
- Signature
因此,一個典型的JWT看起來是這個樣子的:
xxxxx.yyyyy.zzzzz
接下來,具體看一下每一部分:
Header
header典型的由兩部分組成:token的類型(“JWT”)和算法名稱(比如:HMAC SHA256或者RSA等等)。
例如:
然后,用Base64對這個JSON編碼就得到JWT的第一部分
Payload
JWT的第二部分是payload,它包含聲明(要求)。聲明是關於實體(通常是用戶)和其他數據的聲明。聲明有三種類型: registered, public 和 private。
- Registered claims : 這里有一組預定義的聲明,它們不是強制的,但是推薦。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。
- Public claims : 可以隨意定義。
- Private claims : 用於在同意使用它們的各方之間共享信息,並且不是注冊的或公開的聲明。
下面是一個例子:
對payload進行Base64編碼就得到JWT的第二部分
注意,不要在JWT的payload或header中放置敏感信息,除非它們是加密的。
Signature
為了得到簽名部分,你必須有編碼過的header、編碼過的payload、一個秘鑰,簽名算法是header中指定的那個,然對它們簽名即可。
例如:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
簽名是用於驗證消息在傳遞過程中有沒有被更改,並且,對於使用私鑰簽名的token,它還可以驗證JWT的發送方是否為它所稱的發送方。
看一張官網的圖就明白了:
4. JSON Web Tokens是如何工作的
在認證的時候,當用戶用他們的憑證成功登錄以后,一個JSON Web Token將會被返回。此后,token就是用戶憑證了,你必須非常小心以防止出現安全問題。一般而言,你保存令牌的時候不應該超過你所需要它的時間。
無論何時用戶想要訪問受保護的路由或者資源的時候,用戶代理(通常是瀏覽器)都應該帶上JWT,典型的,通常放在Authorization header中,用Bearer schema。
header應該看起來是這樣的:
Authorization: Bearer <token>
服務器上的受保護的路由將會檢查Authorization header中的JWT是否有效,如果有效,則用戶可以訪問受保護的資源。如果JWT包含足夠多的必需的數據,那么就可以減少對某些操作的數據庫查詢的需要,盡管可能並不總是如此。
如果token是在授權頭(Authorization header)中發送的,那么跨源資源共享(CORS)將不會成為問題,因為它不使用cookie。
下面這張圖顯示了如何獲取JWT以及使用它來訪問APIs或者資源:
- 應用(或者客戶端)想授權服務器請求授權。例如,如果用授權碼流程的話,就是/oauth/authorize
- 當授權被許可以后,授權服務器返回一個access token給應用
- 應用使用access token訪問受保護的資源(比如:API)
5. 基於Token的身份認證 與 基於服務器的身份認證
5.1. 基於服務器的身份認證
在討論基於Token的身份認證是如何工作的以及它的好處之前,我們先來看一下以前我們是怎么做的:
HTTP協議是無狀態的,也就是說,如果我們已經認證了一個用戶,那么他下一次請求的時候,服務器不知道我是誰,我們必須再次認證
傳統的做法是將已經認證過的用戶信息存儲在服務器上,比如Session。用戶下次請求的時候帶着Session ID,然后服務器以此檢查用戶是否認證過。
這種基於服務器的身份認證方式存在一些問題:
- Sessions : 每次用戶認證通過以后,服務器需要創建一條記錄保存用戶信息,通常是在內存中,隨着認證通過的用戶越來越多,服務器的在這里的開銷就會越來越大。
- Scalability : 由於Session是在內存中的,這就帶來一些擴展性的問題。
- CORS : 當我們想要擴展我們的應用,讓我們的數據被多個移動設備使用時,我們必須考慮跨資源共享問題。當使用AJAX調用從另一個域名下獲取資源時,我們可能會遇到禁止請求的問題。
- CSRF : 用戶很容易受到CSRF攻擊。
5.2. JWT與Session的差異
相同點是,它們都是存儲用戶信息;然而,Session是在服務器端的,而JWT是在客戶端的。
Session方式存儲用戶信息的最大問題在於要占用大量服務器內存,增加服務器的開銷。
而JWT方式將用戶狀態分散到了客戶端中,可以明顯減輕服務端的內存壓力。
Session的狀態是存儲在服務器端,客戶端只有session id;而Token的狀態是存儲在客戶端。
5.3. 基於Token的身份認證是如何工作的
基於Token的身份認證是無狀態的,服務器或者Session中不會存儲任何用戶信息。
沒有會話信息意味着應用程序可以根據需要擴展和添加更多的機器,而不必擔心用戶登錄的位置。
雖然這一實現可能會有所不同,但其主要流程如下:
- 用戶攜帶用戶名和密碼請求訪問
- 服務器校驗用戶憑據
- 應用提供一個token給客戶端
- 客戶端存儲token,並且在隨后的每一次請求中都帶着它
- 服務器校驗token並返回數據
注意:
- 每一次請求都需要token
- Token應該放在請求header中
- 我們還需要將服務器設置為接受來自所有域的請求,用Access-Control-Allow-Origin: *
5.4. 用Token的好處
- 無狀態和可擴展性:Tokens存儲在客戶端。完全無狀態,可擴展。我們的負載均衡器可以將用戶傳遞到任意服務器,因為在任何地方都沒有狀態或會話信息。
- 安全:Token不是Cookie。(The token, not a cookie.)每次請求的時候Token都會被發送。而且,由於沒有Cookie被發送,還有助於防止CSRF攻擊。即使在你的實現中將token存儲到客戶端的Cookie中,這個Cookie也只是一種存儲機制,而非身份認證機制。沒有基於會話的信息可以操作,因為我們沒有會話!
還有一點,token在一段時間以后會過期,這個時候用戶需要重新登錄。這有助於我們保持安全。還有一個概念叫token撤銷,它允許我們根據相同的授權許可使特定的token甚至一組token無效。
5.5. JWT與OAuth的區別
- OAuth2是一種授權框架 ,JWT是一種認證協議
- 無論使用哪種方式切記用HTTPS來保證數據的安全性
- OAuth2用在使用第三方賬號登錄的情況(比如使用weibo, qq, github登錄某個app),而JWT是用在前后端分離, 需要簡單的對后台API進行保護時使用。
5.6. 關於OAuth可以參考下面幾篇
6. 參考
https://tools.ietf.org/html/rfc7519#section-3
http://blog.leapoahead.com/2015/09/06/understanding-jwt/
https://cnodejs.org/topic/557844a8e3cc2f192486a8ff
http://blog.leapoahead.com/2015/09/07/user-authentication-with-jwt/
歡迎各位轉載,但必須在文章頁面中給出作者和原文鏈接!
jwt原理(json web token)
我們之前是使用session實現登錄,通過實際密碼+鹽組成字符串進行md5存入redis或者數據庫中,輸入的密碼與實際校驗通過,發送給客戶端一個有效時間的token,期間可以實現登錄訪問了
而jwt無需通過redis或者存儲在數據庫中了,直接通過一串字符串就能拿到信息和時間
優勢
'''
解決跨域問題:這種基於Token的訪問策略可以克服cookies的跨域問題。
服務端無狀態可以橫向擴展,Token可完成認證,無需存儲Session。
系統解耦,Token攜帶所有的用戶信息,無需綁定一個特定的認證方案,只需要知道加密的方法和密鑰就可以進行加密解密,有利於解耦。
防止跨站點腳本攻擊,沒有cookie技術,無需考慮跨站請求的安全問題。
'''
劣勢
'''
jwt一旦發送不能撤回發送的信息
不能防止CSRF攻擊等
'''
jwt格式分為三部分組成,header,playload,sign
header就是json格式 {"typ":"JWT","alg":"HS256","exp":1491066992916},typ表示類型為jwt格式,alg表示加密方式為hs256,exp表示時間
playroad是請求體,根據業務自行定義,可以是一個字典一個列表,一個字符串
sign表示簽名的生成
加密原理:
把header和playload分別使用base64url編碼,接着用'.'把兩個編碼后的字符串連接起來,再把這拼接起來的字符串配合密鑰進行HMAC SHA-256算法加密,最后再次base64編碼下,這就拿到了簽名sign. 最后把header和playload和sign用'.'連接起來就生成了整個JWT
解密原理:
后端服務校驗jwtToken是否有權訪問接口服務,進行解密認證,如校驗訪問者的userid,首先用將字符串按.號切分三段字符串,分別得到header和playload和sign。然后將header.playload拼裝用密鑰和HAMC SHA-256算法進行加密然后得到新的字符串和sign進行比對,
如果一樣就代表數據沒有被篡改,然后從頭部取出exp對存活期進行判斷,如果超過了存活期就返回空字符串,如果在存活期內返回userid的值
校驗原理:
整個jwt的結構是由header.playload.sign連接組成,只有sign是用密鑰加密的,而所有的信息都在header和playload中可以直接獲取,sign的作用只是校驗header和playload的信息是否被篡改過,所以jwt不能保護數據,但以上的特性可以很好的應用在權限認證上
itsdangerous是python基於jwt的實現的,我們可以用第三方庫來實現
itsdangerous安裝
pip install itsdangerous
Signer(secret_key, salt=None, sep='.', key_derivation=None, digest_method=None, algorithm=None)
# 簽名加密/解密 from itsdangerous import Signer s = Signer('rainbol') # 設置鹽值 res = s.sign('me').decode() # 設置簽名 print(res) # me.OJZBr7m8IGARJ0qzK07wdy9xAJM 返回一個簽名.字符串 print(s.unsign('me.OJZBr7m8IGARJ0qzK07wdy9xAJM').decode()) # 解簽 返回me ,如果中間任意字符串不正確都會報錯
TimestampSigner(secret_key, salt=None, sep='.', key_derivation=None, digest_method=None, algorithm=None)
# 帶時間簽名加密/解密
from itsdangerous import TimestampSigner s = TimestampSigner('rainbol') # 設置鹽值 string = s.sign('123') # 加簽 print(string.decode()) # 返回123.XNkPLg.Qr0E6jJvj7-tg40SBtC0kunkM1w res = s.unsign(string, max_age=20) # 解簽 max_age設置過期時間20秒 print(res.decode()) # 返回123 ,如果中間任意字符串不正確或者超過max_age時間都會報錯
Serializer(secret_key, salt='itsdangerous', serializer=None, signer=None, signer_kwargs=None)
# 序列化 from itsdangerous import Serializer l = Serializer('rainbol') # 設置鹽值 re = l.dumps(['1314', '2233']) print(re) # ["1314","2233"].V2vY21tK5Nc8wYP6rlY9-F2zhH0 print(l.loads(re)) # ['1314', '2233'] 如果不正確會報錯 # 如果需要加入時間戳,需要加入TimedSerializer,用法和TimestampSigner一致
# url安全序列化 from itsdangerous import URLSafeSerializer l = URLSafeSerializer('rainbol') re = l.dumps(['122', '2233']) # 返回 WyIxMjIiLCIyMjMzIl0.SujNVMlTr3RTkuQIHIRjl1R7tTs 序列化更加安全,其他一致
# jsonweb簽名 from itsdangerous import JSONWebSignatureSerializer s = JSONWebSignatureSerializer('rainbol') # 設置鹽值 res = s.dumps({'rainbol': '123456'}) # 設置加密字典 print(res.decode()) # eyJhbGciOiJIUzUxMiJ9.eyJyYWluYm9sIjoiMTIzNDU2In0.CCMq-9G8HkoMb7NFXHeTSg0iG2lndsyZL-fcrV85gfuXBekNP3AWa3bUTYWWHuiFRsueqZ495S5bMxFCoglxZg res = s.loads('eyJhbGciOiJIUzUxMiJ9.eyJyYWluYm9sIjoiMTIzNDU2In0.CCMq-9G8HkoMb7NFXHe' 'TSg0iG2lndsyZL-fcrV85gfuXBekNP3AWa3bUTYWWHuiFRsueqZ495S5bMxFCoglxZg', return_header=True) # 解密字典 print(res) # 返回{'rainbol': '123456'} 如果中間任意字符串不正確都會報錯
在實際開發過程中用以下方法即可
#帶有時間的jsonweb簽名 import itsdangerous salt = 'saO)(&)H' # 設置鹽值 t = itsdangerous.TimedJSONWebSignatureSerializer(salt, expires_in=600) # 指定參數,第一個是鹽值,第二個是TTL加密過期時間 res = t.dumps({'username': 'rainbol', 'password': '123456'}) # 設置加密的字典 print(res.decode()) # 取加密信息 session = 'eyJhbGciOiJIUzUxMiIsImlhdCI6MTU1NzcyNzM4NywiZXhwIjoxNTU3NzI3OTg3fQ.eyJ1c2VybmFtZSI6InJhaW5ib2wiLCJwYXNzd29yZCI6IjEyMzQ1NiJ9.5r2_YYH06HLCW9Ix7EB5UGpk0wPD8RmWib0EoD9lgZIaRHEaH-qrMfQeKPriQNplD1gNLMM2Dn9NX-zoSz20Gg' res = t.loads(session)#解析加密信息,如果加密信息不正確會報錯 print(res)
參考https://blog.csdn.net/weiker12/article/details/68950279
版權聲明:本文原創發表於 博客園,作者為 RainBol 本文歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則視為侵權。
1.什么是JWT?
JWT全稱JSON Web Token。是為了在網絡應用環境鍵傳遞聲明而執行的一種基於JSON的開放標准。
2.JWT的使用場景?
授權:一旦用戶登錄,每個后續請求將包括JWT,允許用戶訪問該令牌允許的路由,服務和資源。單點登錄是一種在廣泛使用JWT的功能,因為它的開銷很小,並且能夠在不同的域中輕松使用。
信息交換:JSON Web令牌是在各方之間安全傳輸信息的好方法。因為JWT可以簽名 - 例如,使用公鑰/私鑰對 - 您可以確定發件人是他們所說的人。此外,由於使用標頭和有效負載計算簽名,您還可以驗證內容是否未被篡改。
3.JWT的結構組成?
由三部分組成,用(.
)進行分隔,他們是
- 頭部(header)
- 有效載荷(payload)
- 簽名(signature)
因此,JWT通常如下表示
xxxxxx.yyyyy.zzzzz
頭部:
JWT頭部分是一個描述JWT元數據的JSON對象,通常如下所示。
在上面的代碼中,alg屬性表示簽名使用的算法,默認為HMAC SHA256(寫為HS256);typ屬性表示令牌的類型,JWT令牌統一寫為JWT。然后,這個json被編碼稱Base64Url,形成JWT的第一部分。
有效載荷:
有效載荷是JWT的主體內容部分,也是一個JSON對象,包含需要傳遞的數據。JWT指定七個默認字段供選擇。
iss:發行人
exp:到期時間
sup:主題
aud:用戶
nbf:在此之前不可用
iat:發行時間
jti:JWT ID用於標識該JWT
除以上默認字段外,我們還可以自定義私有字段,如下例:
請注意,默認情況下JWT是未加密的,任何人都可以解讀其內容,因此不要構建隱私信息字段,存放保密信息,以防止信息泄露。
JSON對象也使用Base64 URL算法轉換為字符串保存,形成JWT第二部分。
簽名:
簽名是對上面兩部分數據簽名,通過指定的算法生成哈希,以確保數據不會被篡改。
首先,需要制定一個密鑰(secret)。該密鑰僅僅為保存在服務器中,並且不能向用戶公開。然后,使用頭部中指定的簽名算法(默認情況下為HMACSHA256)根據以下公式生成簽名。
在計算出簽名后,將JWT頭部,有效載荷和簽名的三個部分組合成一個字符串,每個部分用"."分隔,就構成整個JWT對象,返回給用戶。
4.JWT的用法?
JWT的原理是,服務器認證以后,生成一個JSON對象,發回給用戶,如下面格式
{
"姓名": "張三",
"角色": "管理員",
"到期時間": "2018年7月1日0點0分"
}
以后用戶與服務端通信的時候,都要返回這個JSON對象。服務器完全只靠這個對象認定用戶身份。為了防止用戶篡改數據,服務器在生成這個對象的時候,會加上簽名。
客戶端收到服務器返回的JWT,可以存儲到Cookie里面,也可以存儲到localStorage。
此后,客戶端每次與服務器通信,都要帶上這個JWT。你可以把它放在cookie里面自動發送,但是這樣就不能實現跨域。所以更好的做法是放在HTTP請求的頭信息Authorization字段里面
另一種做法是,跨域的時候,JWT就放在POST請求的數據里面。
5.JWT特點?
(1)JWT默認是不加密的,但也是可以加密的。生成原始Token以后,可以用密鑰在加密一次。
(2)JWT不加密的情況下,不能將秘密數據寫入JWT。
(3)JWT不僅可以用於認證,也可以死用於交換信息。有效使用JWT,可以降低服務器的查詢數據庫的次數。
(4)JWT最大的缺點是,用於服務器不保存sessi000on狀態,因此無法在使用過程中廢止某個token,或者更改token的權限,也就是說,一旦JWT簽發了,在到期之前就會始終有效,除非服務器部署額外的邏輯。
(5)JWT本身包含了認證信息,一旦泄露,任何人都可以獲得該令牌的所有權限。為了減少盜用,JWT的有效期限應該設置的比較短。對於一些比較重要的權限,使用時應該再次對用戶進行認證。
(6)為了較少盜用,JWT不應該使用HTTP協議明碼傳輸,要使用HTTPS協議傳輸。
7.JWT的入門案例(Java方式)
創建一個項目,先引入依賴。
創建一個Jwtutil工具類,里面有包括創建Jwt和解析Jwt
創建JWT
public String createJWT(String id, String subject, long ttlMillis) throws Exception {
//指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經將這部分內容封裝好了
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成JWT的時間
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//創建payload的私有聲明(根據特定的業務需要添加,如果要拿這個做驗證,一般是需要和jwt的接收方提前溝通好驗證方式的)
Map<String,Object> claims = new HashMap<String,Object>();
claims.put("uid", "DSSFAWDWADAS...");
claims.put("user_name", "admin");
claims.put("nick_name","DASDA121");
/**
* 生成簽名的時候使用的秘鑰secret,這個方法本地封裝了的,一般可以從本地配置文件中讀取,切記這個秘鑰不能外露哦。它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。
*/
SecretKey key = generalKey();
//下面就是在為payload添加各種標准聲明和私有聲明了
//這里其實就是new一個JwtBuilder,設置jwt的body
JwtBuilder builder = Jwts.builder()
//如果有私有聲明,一定要先設置這個自己創建的私有的聲明,這個是給builder的claim賦值,一旦寫在標准的聲明賦值之后,就是覆蓋了那些標准的聲明的
.setClaims(claims)
//設置jti(JWT ID):是JWT的唯一標識,根據業務需要,這個可以設置為一個不重復的值,主要用來作為一次性token,從而回避重放攻擊。
.setId(id)
//iat: jwt的簽發時間
.setIssuedAt(now)
//sub(Subject):代表這個JWT的主體,即它的所有人,這個是一個json格式的字符串,可以存放什么userid,roldid之類的,作為什么用戶的唯一標志。
.setSubject(subject)
//設置簽名使用的簽名算法和簽名使用的秘鑰
.signWith(signatureAlgorithm, key);
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
//設置過期時間
builder.setExpiration(exp);
}
//就開始壓縮為xxxxxxxxxxxxxx.xxxxxxxxxxxxxxx.xxxxxxxxxxxxx這樣的jwt
return builder.compact();
}
解析JWT
public Claims parseJWT(String jwt) throws Exception{
//簽名秘鑰,和生成的簽名的秘鑰一模一樣
SecretKey key = generalKey();
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//設置簽名的秘鑰
.setSigningKey(key)
//設置需要解析的jwt
.parseClaimsJws(jwt).getBody();
//claims相當於一個map,包含了我們想要的信息
return claims;
}
接下來測試一下,首先創建一個jwt,得到一個xxxxxx.zzzzzz.yyyyy字符串
我們再來解析一下這個字符串:
這樣,我們就能夠根據這個字符串解析出用戶信息,從而驗證登錄授權等功能。
什么是JWT
Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標准((RFC 7519).該token被設計為緊湊且安全的,特別適用於分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
簡單來說就是 JWT(Json Web Token)是實現token技術的一種解決方案
為什么使用JWT
token驗證和session認證的區別
傳統的session認證
http協議本身是一種無狀態的協議,而這就意味着如果用戶向我們的應用提供了用戶名和密碼來進行用戶認證,那么下一次請求時,用戶還要再一次進行用戶認證才行,因為根據http協議,我們並不能知道是哪個用戶發出的請求,所以為了讓我們的應用能識別是哪個用戶發出的請求,我們只能在服務器存儲一份用戶登錄的信息,這份登錄信息會在響應時傳遞給瀏覽器,告訴其保存為cookie,以便下次請求時發送給我們的應用,這樣我們的應用就能識別請求來自哪個用戶了,這就是傳統的基於session認證。
session缺點
基於session的認證使應用本身很難得到擴展,隨着不同客戶端用戶的增加,獨立的服務器已無法承載更多的用戶
Session方式存儲用戶id的最大弊病在於要占用大量服務器內存,對於較大型應用而言可能還要保存許多的狀態。
基於session認證暴露的問題
- Session需要在服務器保存,暫用資源
- 擴展性 session認證保存在內存中 ,無法擴展到其他機器中
- CSRF 基於cookie來進行用戶識別的, cookie如果被截獲,用戶就會很容易受到跨站請求偽造的攻擊。
基於token的鑒權機制
基於token的鑒權機制類似於http協議也是無狀態的,它不需要在服務端去保留用戶的認證信息或者會話信息。這就意味着基於token認證機制的應用不需要去考慮用戶在哪一台服務器登錄了,這就為應用的擴展提供了便利。
JWT方式將用戶狀態分散到了客戶端中,可以明顯減輕服務端的內存壓力。除了用戶id之外,還可以存儲其他的和用戶相關的信息,例如用戶角色,用戶性別等。
請求流程
- 用戶使用用戶名密碼來請求服務器
- 服務器進行驗證用戶的信息
- 服務器通過驗證發送給用戶一個token
- 客戶端存儲token,並在每次請求時附送上這個token值
- 服務端驗證token值,並返回數據
這個token必須要在每次請求時傳遞給服務端,它應該保存在請求頭里, 另外,服務端要支持
CORS(跨來源資源共享)
策略,一般我們在服務端這么做就可以了Access-Control-Allow-Origin: *
。
JWT的結構
一個JWT是下面的結構
加密后jwt信息如下所示,是由.分割的三部分組成,分別為Header、Payload、Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT 的組成
Head -主要包含兩個部分,alg指加密類型,可選值為
HS256
、RSA
等等,typ=JWT
為固定值,表示token的類型Header: { "alg": "HS256", "typ": "JWT" }
Payload - Payload又被稱為Claims包含您想要簽署的任何信息
Claims: { "sub": "1234567890", "name": "John Doe", "admin": true }
JWT Payload的組成
Payload通常由三個部分組成,分別是 Registered Claims ; Public Claims ; Private Claims ;每個聲明,都有各自的字段。
Registered Claims
iss 【issuer】發布者的url地址
sub 【subject】該JWT所面向的用戶,用於處理特定應用,不是常用的字段
aud 【audience】接受者的url地址
exp 【expiration】 該jwt銷毀的時間;unix時間戳
nbf 【not before】 該jwt的使用時間不能早於該時間;unix時間戳
iat 【issued at】 該jwt的發布時間;unix 時間戳
jti 【JWT ID】 該jwt的唯一ID編號
Signature 對 則為對Header、Payload的簽名
Signature: base64UrlEncode(Header) + "." + base64UrlEncode(Claims)
頭部、聲明、簽名用 . 號連在一起就得到了我們要的JWT 也就是夏明這種類型的字符串
eyJhbGciOiJIUzI1NiJ9.
eyJleHAiOjE1MTUyOTgxNDEsImtleSI6InZhdWxlIn0.
orewTmil7YmIXKILHwFnw3Bq1Ox4maXEzp0NC5LRaFQ
其實這些事一行的,我只是讓看的更直白點將其割開了。
JAVA 實現
JAVA中使用JWT
使用Maven引入和Gradle引入
Maven
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version> </dependency>
Gradle
dependencies { compile 'io.jsonwebtoken:jjwt:0.9.0' }
JWT依賴於Jackson,需要在程序中加入Jackson的jar包且版本大於2.x
簽發JWT
public static String createJWT() {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
JwtBuilder builder = Jwts.builder()
.setId(id) // JWT_ID
.setAudience("") // 接受者
.setClaims(null) // 自定義屬性
.setSubject("") // 主題
.setIssuer("") // 簽發者
.setIssuedAt(new Date()) // 簽發時間
.setNotBefore(new Date()) // 失效時間
.setExpiration(long) // 過期時間
.signWith(signatureAlgorithm, secretKey); // 簽名算法以及密匙
return builder.compact();
}
驗證JWT
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
完整示例
package com.tingfeng.demo;
import com.google.gson.Gson;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.tomcat.util.codec.binary.Base64;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtUtil {
<span class="hljs-comment">/**
* 由字符串生成加密key
*
* <span class="hljs-doctag">@return</span>
*/</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> SecretKey <span class="hljs-title">generalKey</span><span class="hljs-params">()</span> </span>{
String stringKey = Constant.JWT_SECRET;
<span class="hljs-comment">// 本地的密碼解碼</span>
<span class="hljs-keyword">byte</span>[] encodedKey = Base64.decodeBase64(stringKey);
<span class="hljs-comment">// 根據給定的字節數組使用AES加密算法構造一個密鑰</span>
SecretKey key = <span class="hljs-keyword">new</span> SecretKeySpec(encodedKey, <span class="hljs-number">0</span>, encodedKey.length, <span class="hljs-string">"AES"</span>);
<span class="hljs-keyword">return</span> key;
}
<span class="hljs-comment">/**
* 創建jwt
* <span class="hljs-doctag">@param</span> id
* <span class="hljs-doctag">@param</span> issuer
* <span class="hljs-doctag">@param</span> subject
* <span class="hljs-doctag">@param</span> ttlMillis
* <span class="hljs-doctag">@return</span>
* <span class="hljs-doctag">@throws</span> Exception
*/</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">createJWT</span><span class="hljs-params">(String id, String issuer, String subject, <span class="hljs-keyword">long</span> ttlMillis)</span> <span class="hljs-keyword">throws</span> Exception </span>{
<span class="hljs-comment">// 指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經將這部分內容封裝好了。</span>
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
<span class="hljs-comment">// 生成JWT的時間</span>
<span class="hljs-keyword">long</span> nowMillis = System.currentTimeMillis();
Date now = <span class="hljs-keyword">new</span> Date(nowMillis);
<span class="hljs-comment">// 創建payload的私有聲明(根據特定的業務需要添加,如果要拿這個做驗證,一般是需要和jwt的接收方提前溝通好驗證方式的)</span>
Map<String, Object> claims = <span class="hljs-keyword">new</span> HashMap<>();
claims.put(<span class="hljs-string">"uid"</span>, <span class="hljs-string">"123456"</span>);
claims.put(<span class="hljs-string">"user_name"</span>, <span class="hljs-string">"admin"</span>);
claims.put(<span class="hljs-string">"nick_name"</span>, <span class="hljs-string">"X-rapido"</span>);
<span class="hljs-comment">// 生成簽名的時候使用的秘鑰secret,切記這個秘鑰不能外露哦。它就是你服務端的私鑰,在任何場景都不應該流露出去。</span>
<span class="hljs-comment">// 一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。</span>
SecretKey key = generalKey();
<span class="hljs-comment">// 下面就是在為payload添加各種標准聲明和私有聲明了</span>
JwtBuilder builder = Jwts.builder() <span class="hljs-comment">// 這里其實就是new一個JwtBuilder,設置jwt的body</span>
.setClaims(claims) <span class="hljs-comment">// 如果有私有聲明,一定要先設置這個自己創建的私有的聲明,這個是給builder的claim賦值,一旦寫在標准的聲明賦值之后,就是覆蓋了那些標准的聲明的</span>
.setId(id) <span class="hljs-comment">// 設置jti(JWT ID):是JWT的唯一標識,根據業務需要,這個可以設置為一個不重復的值,主要用來作為一次性token,從而回避重放攻擊。</span>
.setIssuedAt(now) <span class="hljs-comment">// iat: jwt的簽發時間</span>
.setIssuer(issuer) <span class="hljs-comment">// issuer:jwt簽發人</span>
.setSubject(subject) <span class="hljs-comment">// sub(Subject):代表這個JWT的主體,即它的所有人,這個是一個json格式的字符串,可以存放什么userid,roldid之類的,作為什么用戶的唯一標志。</span>
.signWith(signatureAlgorithm, key); <span class="hljs-comment">// 設置簽名使用的簽名算法和簽名使用的秘鑰</span>
<span class="hljs-comment">// 設置過期時間</span>
<span class="hljs-keyword">if</span> (ttlMillis >= <span class="hljs-number">0</span>) {
<span class="hljs-keyword">long</span> expMillis = nowMillis + ttlMillis;
Date exp = <span class="hljs-keyword">new</span> Date(expMillis);
builder.setExpiration(exp);
}
<span class="hljs-keyword">return</span> builder.compact();
}
<span class="hljs-comment">/**
* 解密jwt
*
* <span class="hljs-doctag">@param</span> jwt
* <span class="hljs-doctag">@return</span>
* <span class="hljs-doctag">@throws</span> Exception
*/</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> Claims <span class="hljs-title">parseJWT</span><span class="hljs-params">(String jwt)</span> <span class="hljs-keyword">throws</span> Exception </span>{
SecretKey key = generalKey(); <span class="hljs-comment">//簽名秘鑰,和生成的簽名的秘鑰一模一樣</span>
Claims claims = Jwts.parser() <span class="hljs-comment">//得到DefaultJwtParser</span>
.setSigningKey(key) <span class="hljs-comment">//設置簽名的秘鑰</span>
.parseClaimsJws(jwt).getBody(); <span class="hljs-comment">//設置需要解析的jwt</span>
<span class="hljs-keyword">return</span> claims;
}
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> </span>{
User user = <span class="hljs-keyword">new</span> User(<span class="hljs-string">"tingfeng"</span>, <span class="hljs-string">"bulingbuling"</span>, <span class="hljs-string">"1056856191"</span>);
String subject = <span class="hljs-keyword">new</span> Gson().toJson(user);
<span class="hljs-keyword">try</span> {
JwtUtil util = <span class="hljs-keyword">new</span> JwtUtil();
String jwt = util.createJWT(Constant.JWT_ID, <span class="hljs-string">"Anson"</span>, subject, Constant.JWT_TTL);
System.out.println(<span class="hljs-string">"JWT:"</span> + jwt);
System.out.println(<span class="hljs-string">"\n解密\n"</span>);
Claims c = util.parseJWT(jwt);
System.out.println(c.getId());
System.out.println(c.getIssuedAt());
System.out.println(c.getSubject());
System.out.println(c.getIssuer());
System.out.println(c.get(<span class="hljs-string">"uid"</span>, String.class));
} <span class="hljs-keyword">catch</span> (Exception e) {
e.printStackTrace();
}
}
}
Constant.java
package com.tingfeng.demo;
import java.util.UUID;
public class Constant {
public static final String JWT_ID = UUID.randomUUID().toString();
<span class="hljs-comment">/**
* 加密密文
*/</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String JWT_SECRET = <span class="hljs-string">"woyebuzhidaoxiediansha"</span>;
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">int</span> JWT_TTL = <span class="hljs-number">60</span>*<span class="hljs-number">60</span>*<span class="hljs-number">1000</span>; <span class="hljs-comment">//millisecond</span>
}
輸出示例
JWT:eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiIxMjM0NTYiLCJzdWIiOiJ7XCJuaWNrbmFtZVwiOlwidGluZ2ZlbmdcIixcIndlY2hhdFwiOlwiYnVsaW5nYnVsaW5nXCIsXCJxcVwiOlwiMTA1Njg1NjE5MVwifSIsInVzZXJfbmFtZSI6ImFkbWluIiwibmlja19uYW1lIjoiWC1yYXBpZG8iLCJpc3MiOiJBbnNvbiIsImV4cCI6MTUyMjMxNDEyNCwiaWF0IjoxNTIyMzEwNTI0LCJqdGkiOiJhNGQ5MjA0Zi1kYjM3LTRhZGYtODE0NS1iZGNmMDAzMzFmZjYifQ.B5wdY3_W4MZLj9uBHSYalG6vmYwdpdTXg0otdwTmU4U
解密
a4d9204f-db37-4adf-8145-bdcf00331ff6
Thu Mar 29 16:02:04 CST 2018
{"nickname":"tingfeng","wechat":"bulingbuling","qq":"1056856191"}
Anson
123456
總結
優點
- 因為json的通用性,所以JWT是可以進行跨語言支持的,像JAVA,JavaScript,NodeJS,PHP等很多語言都可以使用。
- 因為有了payload部分,所以JWT可以在自身存儲一些其他業務邏輯所必要的非敏感信息。
- 便於傳輸,jwt的構成非常簡單,字節占用很小,所以它是非常便於傳輸的。
- 它不需要在服務端保存會話信息, 所以它易於應用的擴展
安全相關
- 不應該在jwt的payload部分存放敏感信息,因為該部分是客戶端可解密的部分。
- 保護好secret私鑰,該私鑰非常重要。
- 如果可以,請使用https協議
參考文章
http://www.ibloger.net/article/3075.html
jjwt-gitHub:https://github.com/jwtk/jjwt
https://blog.csdn.net/u012240455/article/details/79019825
https://blog.csdn.net/u012017645/article/details/53585872
http://itindex.net/detail/58305-jwt-json-web
http://itindex.net/detail/58305-jwt-json-web
歡迎訪問我的github github博客地址