之前寫過一篇關於接口服務規範的文章,原文在此,裏面關於安全性問題重點講述了經過appid
,appkey
,timestamp
,nonce
以及sign
來獲取token
,使用token
來保障接口服務的安全。今天咱們來說述一種更加便捷的方式,使用jwt
來生成token。java
JSON Web Token
(JWT
) 定義了一種緊湊且自包含的方式,用於在各方之間做爲 JSON 對象安全地傳輸信息。該信息能夠被驗證和信任,由於它是通過數字簽名的。JWT
能夠設置有效期。web
JWT
是一個很長的字符串,包含了Header
,Playload
和Signature
三部份內容,中間用.
進行分隔。redis
Headers算法
Headers
部分描述的是JWT
的基本信息,通常會包含簽名算法和令牌類型,數據以下:json
{ "alg": "RS256", "typ": "JWT" }
Playload安全
Playload
就是存放有效信息的地方,JWT
規定了如下7個字段,建議但不強制使用:app
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
的第三部分。ui
在應用服務中引入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(); }
上面講述了關於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
。