以前寫過一篇關於介面服務規範的文章,原文在此,裡面關於安全性問題重點講述了通過appid
,appkey
,timestamp
,nonce
以及sign
來獲取token
,使用token
來保障介面服務的安全。今天我們來講述一種更加便捷的方式,使用jwt
來生成token。
一、JWT是什麼
JSON Web Token
(JWT
) 定義了一種緊湊且自包含的方式,用於在各方之間作為 JSON 物件安全地傳輸資訊。該資訊可以被驗證和信任,因為它是經過數字簽名的。JWT
可以設定有效期。
JWT
是一個很長的字串,包含了Header
,Playload
和Signature
三部分內容,中間用.
進行分隔。
Headers
Headers
部分描述的是JWT
的基本資訊,一般會包含簽名演算法和令牌型別,資料如下:
{
"alg": "RS256",
"typ": "JWT"
}
Playload
Playload
就是存放有效資訊的地方,JWT
規定了以下7個欄位,建議但不強制使用:
iss: jwt簽發者
sub: jwt所面向的使用者
aud: 接收jwt的一方
exp: jwt的過期時間,這個過期時間必須要大於簽發時間
nbf: 定義在什麼時間之前,該jwt都是不可用的
iat: jwt的簽發時間
jti: jwt的唯一身份標識,主要用來作為一次性token
除此之外,我們還可以自定義內容
{
"name":"Java旅途",
"age":18
}
Signature
Signature
是將JWT
的前面兩部分進行加密後的字串,將Headers
和Playload
進行base64
編碼後使用Headers
中規定的加密演算法和金鑰進行加密,得到JWT
的第三部分。
二、JWT生成和解析token
在應用服務中引入JWT
的依賴
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
根據JWT
的定義生成一個使用RSA
演算法加密的,有效期為30分鐘
的token
public static String createToken(User user) throws Exception{
return Jwts.builder()
.claim("name",user.getName())
.claim("age",user.getAge())
// rsa加密
.signWith(SignatureAlgorithm.RS256, RsaUtil.getPrivateKey(PRIVATE_KEY))
// 有效期30分鐘
.setExpiration(DateTime.now().plusSeconds(30 * 60).toDate())
.compact();
}
登入介面驗證通過後,呼叫JWT
生成帶有使用者標識的token響應給使用者,在接下來的請求中,頭部攜帶token
進行驗籤,驗籤通過後,正常訪問應用服務。
public static Claims parseToken(String token) throws Exception{
return Jwts
.parser()
.setSigningKey(RsaUtil.getPublicKey(PUBLIC_KEY))
.parseClaimsJws(token)
.getBody();
}
三、token續簽問題
上面講述了關於JWT
驗證的過程,現在我們考慮這樣一個問題,客戶端攜帶token
訪問下單介面,token
驗籤通過,客戶端下單成功,返回下單結果,然後客戶端帶著token
呼叫支付介面進行支付,驗籤的時候發現token失效了,這時候應該怎麼辦?只能告訴使用者token
失效,然後讓使用者重新登入獲取token
?這種體驗是非常不好的,oauth2
在這方面做的比較好,除了簽發token
,還會簽發refresh_token
,當token
過期後,會去呼叫refresh_token
重新獲取token
,如果refresh_token
也過期了,那麼再提示使用者去登入。現在我們模擬oauth2
的實現方式來完成JWT
的refresh_token
。
思路大概就是使用者登入成功後,簽發token
的同時,生成一個加密串作為refresh_token
,refresh_token
存放在redis
中,設定合理的過期時間(一般會將refresh_token
的過期時間設定的比較久一點)。然後將token
和refresh_token
響應給客戶端。虛擬碼如下:
@PostMapping("getToken")
public ResultBean getToken(@RequestBody LoingUser user){
ResultBean resultBean = new ResultBean();
// 使用者資訊校驗失敗,響應錯誤
if(!user){
resultBean.fillCode(401,"賬戶密碼不正確");
return resultBean;
}
String token = null;
String refresh_token = null;
try {
// jwt 生成的token
token = JwtUtil.createToken(user);
// 重新整理token
refresh_token = Md5Utils.hash(System.currentTimeMillis()+"");
// refresh_token過期時間為24小時
redisUtils.set("refresh_token:"+refresh_token,token,30*24*60*60);
} catch (Exception e) {
e.printStackTrace();
}
Map<String,Object> map = new HashMap<>();
map.put("access_token",token);
map.put("refresh_token",refresh_token);
map.put("expires_in",2*60*60);
resultBean.fillInfo(map);
return resultBean;
}
客戶端呼叫介面時,在請求頭中攜帶token
,在攔截器中攔截請求,驗證token
的有效性,如果驗證token
失敗,則去redis中判斷是否是refresh_token
的請求,如果refresh_token
驗證也失敗,則給客戶端響應鑑權異常,提示客戶端重新登入,虛擬碼如下:
HttpHeaders headers = request.getHeaders();
// 請求頭中獲取令牌
String token = headers.getFirst("Authorization");
// 判斷請求頭中是否有令牌
if (StringUtils.isEmpty(token)) {
resultBean.fillCode(401,"鑑權失敗,請攜帶有效token");
return resultBean;
}
if(!token.contains("Bearer")){
resultBean.fillCode(401,"鑑權失敗,請攜帶有效token");
return resultBean;
}
token = token.replace("Bearer ","");
// 如果請求頭中有令牌則解析令牌
try {
Claims claims = TokenUtil.parseToken(token).getBody();
} catch (Exception e) {
e.printStackTrace();
String refreshToken = redisUtils.get("refresh_token:" + token)+"";
if(StringUtils.isBlank(refreshToken) || "null".equals(refreshToken)){
resultBean.fillCode(403,"refresh_token已過期,請重新獲取token");
return resultbean;
}
}
refresh_token
來換取token
的虛擬碼如下:
@PostMapping("refreshToken")
public Result refreshToken(String token){
ResultBean resultBean = new ResultBean();
String refreshToken = redisUtils.get(TokenConstants.REFRESHTOKEN + token)+"";
String access_token = null;
try {
Claims claims = JwtUtil.parseToken(refreshToken);
String username = claims.get("username")+"";
String password = claims.get("password")+"";
LoginUser loginUser = new LoginUser();
loginUser.setUsername(username);
loginUser.setPassword(password);
access_token = JwtUtil.createToken(loginUser);
} catch (Exception e) {
e.printStackTrace();
}
Map<String,Object> map = new HashMap<>();
map.put("access_token",access_token);
map.put("refresh_token",token);
map.put("expires_in",30*60);
resultBean.fillInfo(map);
return resultBean;
}
通過上面的分析,我們簡單的實現了token
的簽發,驗籤以及續簽問題,JWT
作為一個輕量級的鑑權框架,使用起來非常方便,但是也會存在一些問題,
-
JWT
的Playload
部分只是經過base64編碼,這樣我們的資訊其實就完全暴露了,一般不要將敏感資訊存放在JWT
中。 -
JWT
生成的token
比較長,每次在請求頭中攜帶token
,導致請求偷會比較大,有一定的效能問題。 -
JWT
生成後,服務端無法廢棄,只能等待JWT
主動過期。
下面這段是我網上看到的一段關於JWT
比較適用的場景:
-
有效期短
-
只希望被使用一次
比如,使用者註冊後發一封郵件讓其啟用賬戶,通常郵件中需要有一個連結,這個連結需要具備以下的特性:能夠標識使用者,該連結具有時效性(通常只允許幾小時之內啟用),不能被篡改以啟用其他可能的賬戶,一次性的。這種場景就適合使用JWT
。