JWT公司的主流Json Web Token 令牌 如何使用感憾,取代session竿屹,還可以運用分布式認證
JWT簡介:
JWT(JSON WEB TOKEN):JSON網(wǎng)絡(luò)令牌,JWT是一個輕便的安全跨平臺傳輸格式缸榛,定義了一個緊湊的自包含的方式在不同實體之間安全傳輸信息(JSON格式)蚊俺。它是在Web環(huán)境下兩個實體之間傳輸數(shù)據(jù)的一項標(biāo)準(zhǔn)尿背。實際上傳輸?shù)木褪且粋€字符串。廣義上講JWT是一個標(biāo)準(zhǔn)的名稱橡伞;狹義上JWT指的就是用來傳遞的那個token字符串
jwt特點:
- JWT無需存儲在服務(wù)器(不使用Session/Cookie)盒揉,不占用服務(wù)器資源(也就是
Stateless無狀態(tài)
的),也就不存在多服務(wù)器共享Session的問題 - 使用簡單兑徘,用戶在
登錄
成功拿到Token
后刚盈,一般訪問需要權(quán)限的請求時,在Header附上Token即可挂脑。
**JWT公司官網(wǎng): **
jwt的token結(jié)構(gòu):
JWT的數(shù)據(jù)結(jié)構(gòu)以及簽發(fā)的過程
JWT由三部分構(gòu)成:header(頭部)藕漱、payload(載荷)和signature(簽名)。
xxxxx.yyyyy.zzzzz
1崭闲、Header 頭部信息:指定類型和算法
{
"alg": "HS256",
"typ": "JWT"
}
typ:用來標(biāo)識整個token字符串是一個JWT字符串
alg:用來說明這個JWT簽發(fā)的時候所使用的簽名和摘要算法
常用的值以及對應(yīng)的算法如下:
一般簽發(fā)JWT的時候肋联,header對應(yīng)的json結(jié)構(gòu)只需要typ和alg屬性就夠了。JWT的header部分是把前面的json結(jié)構(gòu)镀脂,經(jīng)過Base64Url編碼之后生成出來的
2牺蹄、Payload 荷載信息:存放Claims聲明信息既主體信息組成。用來存儲JWT基本信息薄翅,或者是我們的信息沙兰。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
payload用來承載要傳遞的數(shù)據(jù),它的json結(jié)構(gòu)實際上是對JWT要傳遞的數(shù)據(jù)的一組聲明翘魄,這些聲明被JWT標(biāo)準(zhǔn)稱為claims鼎天。當(dāng)后面對JWT進行驗證的時候,這些claim都能發(fā)揮特定的作用暑竟。
sub代表這個token的所有人斋射,存儲的是所有人的ID育勺;
name表示這個所有人的名字;
admin表示所有人是否管理員的角色罗岖。
根據(jù)JWT的標(biāo)準(zhǔn)涧至,這些claims可以分為以下三種類型:
- Reserved claims
保留的claims都是可選的,JWT標(biāo)準(zhǔn)里面針對它自己規(guī)定的claim都提供了有詳細的驗證規(guī)則描述桑包,每個實現(xiàn)庫都會參照這個描述來提供JWT的驗證實現(xiàn)南蓬。
JWT標(biāo)準(zhǔn)的claim:
屬性 | 全稱 | 描述 | 認證 |
---|---|---|---|
iss | Issuser | 代表簽發(fā)主體 | 完全匹配 |
sub | Subject | 代表的主體递鹉,它的所有人 | 完全匹配 |
aud | Audience | 代表JWT的接收對象 | 包含即可 |
exp | Expiration time | 代表這個JWT的過期時間 | 過期失敗 |
nbf | Not Before | 生效時間 | 早于該時間失敗 |
iat | Issued at | 簽發(fā)時間盼忌,maxAge之類的驗證 | 大于指定值失敗 |
jti | JWT ID | JWT的唯一標(biāo)識 | 完全匹配 |
Public claims
不重要Private claims
自定義的claim不會驗證,除非明確告訴接收方要對這些claim進行驗證以及規(guī)則才行刀荒;標(biāo)準(zhǔn)的claim知道如何進行驗證弱左。
把這個json結(jié)構(gòu)做base64url編碼之后窄陡,就能生成payload部分的串:
3、Signature
把前兩者對應(yīng)的Json結(jié)構(gòu)進行base64url編碼之后的字符串拼接起來和密鑰放一起加密后的簽名拆火,驗證是否是我們服務(wù)器發(fā)起的Token跳夭,secret是我們的密鑰。使用base64拼接很容易破解榜掌,所以建議在傳輸過程中采用ssl加密是最穩(wěn)妥的优妙。
組成方式為: header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiLlvKDlvLoiLCJuYW1lIjoiNzg5IiwiZXhwIjoxNTI4MzY0MjU5LCJpYXQiOjE1MjgzNjMwNTl9.574koY-c9SqMNNzfvAWQuKEnimWeZAcoFQ5XudNWF3o
下面例子使用HMAC SHA256算法進行簽名
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
拿前面的header和payload串來測試,并把“secret”這個字符串就當(dāng)成密鑰來測試:
最后的結(jié)果B其實就是JWT需要的signature憎账。不過對比我在介紹JWT的開始部分給出的JWT的舉例:
在線工具生成的signature有細微區(qū)別套硼,在于最后是否有“=”字符。這些實現(xiàn)庫最后編碼用base64url編碼胞皱,在線工具都是bas64編碼邪意,導(dǎo)致編碼結(jié)果有區(qū)別。
JWT的驗證過程
WT的驗證規(guī)則主要包括簽名驗證和payload里面各個標(biāo)準(zhǔn)claim的驗證反砌。驗證成功的JWT雾鬼,才能當(dāng)做有效的憑證來使用。
- 簽名驗證:對JWT的完整性進行驗證就是簽名認證宴树。
- 驗證方法:把header做base64url解碼策菜,獲取JWT簽名算法,用該算法再次用同樣的邏輯對header和payload做一次簽名酒贬。獲得的串與JWT本身的第三個部分的串比較又憨。不同認為這個JWT被篡改過,驗證失敗了锭吨。
- 生成簽名:接收方生成簽名須使用跟JWT發(fā)送方相同的密鑰蠢莺,做好密鑰的安全傳遞或共享。
簽名認證是每個實現(xiàn)庫都會自動做的零如,但是payload的認證是由使用者來決定的躏将。因為JWT里面可能不會包含任何一個標(biāo)準(zhǔn)的claim锄弱,所以它不會自動去驗證這些claim。
登錄認證在簽發(fā)JWT時祸憋,可以用sub存儲用戶的id会宪,用exp存儲它本次登錄之后的過期時間,然后在驗證的時候僅驗證exp這個claim蚯窥,以實現(xiàn)會話的有效期管理狈谊。
JWT 令牌注銷或者銷毀
痛點:當(dāng)用戶點擊了“注銷”按鈕,用戶的令牌在客戶端會從授權(quán)認證服務(wù)移除沟沙,但此令牌仍舊是有效,可以被攻擊者竊取到用于API調(diào)用壁榕,直至jsonwebtoken的有效時間結(jié)束矛紫。
解決方案:利用Redis撤銷JSON Web Token產(chǎn)生的令牌,當(dāng)用戶點擊注銷按鈕時牌里。且令牌在Redis存儲的時間與令牌在jsonwebtoken中定義的有效時間相同颊咬。當(dāng)有效時間到了后,令牌會自動被Redis刪除牡辽。應(yīng)用檢查各終端上傳的令牌在Redis中是否存在喳篇。
客戶端發(fā)起請求
服務(wù)器端獲取Token
你該如何去理解JWT幫你做些什么?
首先前端登錄后端通過用戶名與密碼驗證成功后,使用jwt生成一個具備有效期的token令牌, 這個token里面存儲著 token本身的結(jié)構(gòu)态辛,加上token的過期時間麸澜,和一個自己定義的字符串類似于加密簽名所用的鹽,這個鹽只有你自己知道奏黑,還可以包含一些自定義的用戶信息放在荷載信息里炊邦。
所涉及的jar包如下:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- 阿里巴巴json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.4</version>
</dependency>
生成Token
public class JwtUtil {
private static Logger logger = Logger.getLogger(JwtUtil.class);
token的key 也是名 不要寫成token這樣,要按照規(guī)范來
public static final String TOKEN_HEADER = "Authorization";
token值的前綴熟史,這是一種規(guī)范
public static final String TOKEN_PREFIX = "Bearer ";
加密時候用 是對稱的秘鑰鹽
private static final String SECRET = "mrLang";
獲取用戶的功能使用的key
public static final String FUNCTS = "FUNCTS";
獲取用戶使用的key
public static final String USERINFO = "USER";
token的生命周期30分
private static final long EXPIRATION = 1800L;
/**
* 創(chuàng)建token令牌 以下為參數(shù)都是自定義信息
* @param loginName 一般我們放用戶的唯一標(biāo)識登錄名
* @param functs 當(dāng)前用戶的功能集合馁害,
* 本人的rbac權(quán)限比較個性化且很負責(zé),一般你們放role角色就可以了
* @param user 當(dāng)前用戶
* @return
*/
public static String createToken(String loginName,
List<Object> functs, Users user) {
Map<String, Object> map = new HashMap<>(); //當(dāng)前用戶擁有的功能
map.put(FUNCTS, JsonUtil.set(functs)); //當(dāng)前用戶信息
map.put(USERINFO, JsonUtil.set(user));
設(shè)置HS256加密蹂匹,并且把你的鹽 放里碘菜,這里推薦使用SH256證書加密
String token = Jwts.builder()
.setSubject(loginName)//主題 主角是誰? 賦值登錄名
.setClaims(map)
.setIssuedAt(new Date())//設(shè)置發(fā)布時間限寞,也是生成時間
.setExpiration(new Date(System.currentTimeMillis()
+ EXPIRATION * 1000))//設(shè)置過期時間
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();//創(chuàng)建完成
return token;
}
公共獲取自定義數(shù)據(jù)
public static Claims getTokenBody(String token) {
return Jwts.parser().setSigningKey(SECRET)
.parseClaimsJws(token).getBody();
}
驗證Token是否過期
public static boolean isExpiration(String token) {
try {
return getTokenBody(token).getExpiration().before(new Date());
} catch (Exception e) {
return true;
}
}
獲取自定義的信息
// 獲取主角忍啸,登錄名
public static String getUserName(String token) {
return getTokenBody(token).getSubject();
}
// 獲取token中存儲的功能
public static List<Object> getUserFuncts(String token) {
String str = getTokenBody(token).get(FUNCTS).toString();
List<Object> list = JsonUtil.getArray(str);
return list;
}
// 獲取token存儲的用戶
public static Object getUser(String token) {
String str = getTokenBody(token).get(USERINFO).toString();
return JsonUtil.getObj(str);
}
刷新Token
public static String refreshToken(String token) {
if (isExpiration(token)) {
logger.info("token刷新失敗@ニ浮吊骤! 過期了!静尼!"); return null;
}
// 獲取用戶 權(quán)限信息
String functs = getTokenBody(token).get(FUNCTS).toString();
String user = getTokenBody(token).get(USERINFO).toString();
String username = getTokenBody(token).getSubject();
Map<String, Object> map = new HashMap<>();
map.put(FUNCTS, JsonUtil.set(functs));
map.put(USERINFO, JsonUtil.set(user));
token = Jwts.builder().signWith(SignatureAlgorithm.HS256, SECRET)
.setClaims(map).setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
.compact();
return token;
}
token發(fā)送給前端
傳入當(dāng)前用戶的功能與用戶信息白粉,登錄名生成token传泊,寫入response的返回頭中,前端獲取后保存在前端的本地緩存中鸭巴,后續(xù)前端請求要把token放在頭header里眷细。
//登錄成功之后
List<Object> functs=(List<Object>) authResult.getAuthorities();
//當(dāng)前功能列表
String loginName=authResult.getName();//登錄名
Users obj=(Users)authResult.getPrincipal();//用戶信息
String token=JwtUtil.createToken(loginName,functs,obj);
//生成token TOKEN_HEADER= Authorization TOKEN_PREFIX=Bearer token值
response.setHeader(JwtUtil.TOKEN_HEADER,JwtUtil.TOKEN_PREFIX+token);
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK); //個人編寫的視圖對象
DTO dto=new DTO<>();
dto.setCode("000000");
dto.setMessage("認證通過");
PrintWriter pw=response.getWriter();
pw.write(JsonUtil.set(dto));//寫入json
pw.flush();//強制刷新
pw.close();//關(guān)閉流
驗證用戶請求攜帶token
你可以自定義一個過濾器,或者使用某某框架鹃祖,自定義攔截器或者aop溪椎。
String header = request.getHeader(JwtUtil.TOKEN_HEADER);
if (null == header || !header.toLowerCase().startsWith(JwtUtil.TOKEN_PREFIX)) {
// 如果頭部 Authorization 未設(shè)置或者不是 basic 認證頭部,則當(dāng)前
// 請求不是該過濾器關(guān)注的對象恬口,直接放行校读,繼續(xù)filter chain 的執(zhí)行
chain.doFilter(request, response);
return;
}
try {
String token = header.replace(JwtUtil.TOKEN_PREFIX, "");
// 驗證token是否過期
if (JwtUtil.isExpiration(token)) {
throw new javax.security.sasl.AuthenticationException("token 驗證不通過");
}
//檢查token是否能解析
Users user = (Users) JwtUtil.getUser(token);
if (null == user) {
throw new javax.security.sasl.AuthenticationException("token 驗證不通過");
}
//驗證成功
JWT簽名算法中HS256和RS256有什么區(qū)別?
JWT簽名算法:HS256 和 RS256祖能。
簽名實際上是一個加密的過程歉秫,生成一段標(biāo)識(也是JWT的一部分)作為接收方驗證信息是否被篡改的依據(jù)。
RS256 (采用SHA-256 的 RSA 簽名) 是一種非對稱算法, 它使用公共/私鑰對: 標(biāo)識提供方采用私鑰生成簽名, JWT 的使用方獲取公鑰以驗證簽名养铸。由于公鑰 (與私鑰相比) 不需要保護雁芙,因此大多數(shù)標(biāo)識提供方使其易于使用方獲取和使用 (通常通過一個元數(shù)據(jù)URL)。
HS256 (帶有 SHA-256 的 HMAC對稱算法, 雙方之間僅共享一個密鑰钞螟。由于使用相同的密鑰生成簽名和驗證簽名兔甘,因此必須注意確保密鑰不被泄密。
在開發(fā)應(yīng)用的時候啟用JWT鳞滨,使用RS256更加安全洞焙,你可以控制誰能使用什么類型的密鑰。另外拯啦,如果你無法控制客戶端闽晦,無法做到密鑰的完全保密,RS256會是個更佳的選擇提岔,JWT的使用方只需要知道公鑰仙蛉。
建議:
為了雙重保險,建議您使用RS256碱蒙,最好是生成一個證書讀取使用證書里面的公鑰加密荠瘪,私鑰留著以后每次前端請求驗證token的合法性就可以了
jwt提供了方法可以驗證token的合法性,過期時間赛惩,還可以根據(jù)token讀取用戶信息哀墓,這樣你把token存儲前端的localstorage中,
后端的緩存都省了
這樣做安全么喷兼?
你要知道 RS256非對稱加密都是一套體系的篮绰,公鑰私鑰是一組, 都存在的后端季惯,別人不進你服務(wù)器盜取你就是安全的吠各,要是真能登錄你服務(wù)器那就直接刪你庫了臀突,同樣本文用大的對稱加密也是 你自定定義的秘鑰 鹽 本文的mrlang都是存在你自己的服務(wù)器中,別人是不知道的贾漏,
分布式中如何驗證Token候学?
你可以把token工具類copy到不同的機器上,讓每臺機器自己去做驗證纵散,只要你每臺機器token工具類里存儲的 鹽或者一對秘鑰 都一致 那么驗證就會得到一致通過 梳码,這樣做的好處就是 你不必遠調(diào)用遠程驗證服務(wù)器了,所以真的很好用伍掀,換個思維想想 你去調(diào)用驗證服務(wù)器還需要花費很多時間掰茶,這對訪問量特別大的項目來說,壓力真的不小蜜笤,所以本文的JWT Token 不管作為單機符匾,集群,分布式 你都值得擁有