如何利用jwt來保護你的接口服務
前言
以前寫過一篇關於接口服務規范的文章,原文在此,裡面關於安全性問題重點講述瞭通過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。
總結
到此這篇關於如何利用jwt來保護你的接口服務的文章就介紹到這瞭,更多相關jwt保護接口服務內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- JWT 設置token過期時間無效的解決
- go gin+token(JWT)驗證實現登陸驗證
- golang中gin框架接入jwt使用token驗證身份
- SpringBoot結合JWT登錄權限控制的實現
- Java JWT實現跨域身份驗證方法詳解