JWT 的全稱為Json Web Token,簡而言之json類型的web服務身份認證令牌(個人理解喲,勿噴)。
適合做前后端分離身份認證,集群服務身份認證,系統群單點登錄等。
1、學習這個技術之前先來了解一下這個技術有哪些優點吧。(參考:https://blog.csdn.net/qq_34037264/article/details/108273333)
1 1、支持跨域訪問: Cookie是不允許垮域訪問的,這一點對Token機制是不存在的,前提是傳輸的用戶認證信息通過HTTP頭傳輸. 2 3 2、無狀態(也稱:服務端可擴展行):Token機制在服務端不需要存儲session信息,因為Token 自身包含了所有登錄用戶的信息,只需要在客戶端的cookie或本地介質存儲狀態信息. 4 5 4、更適用CDN: 可以通過內容分發網絡請求你服務端的所有資料(如:javascript,HTML,圖片等),而你的服務端只要提供API即可.(居於前面兩點得出這個更適用於CDN內容分發網絡) 6 7 5、去耦: 不需要綁定到一個特定的身份驗證方案。Token可以在任何地方生成,只要在你的API被調用的時候,你可以進行Token生成調用即可.(這個似乎也在繼續說前面第一點和第二點的好處。。。) 8 9 6、更適用於移動應用: 當你的客戶端是一個原生平台(iOS, Android,Windows 8等)時,Cookie是不被支持的(你需要通過Cookie容器進行處理),這時采用Token認證機制就會簡單得多。 10 11 7、CSRF:因為不再依賴於Cookie,所以你就不需要考慮對CSRF(跨站請求偽造)的防范。(如果token是用cookie保存,CSRF還是需要考慮,一般建議使用1、在HTTP請求中以參數的形式加入一個服務器端產生的token。或者2.放入http請求頭中也就是一次性給所有該類請求加上csrftoken這個HTTP頭屬性,並把token值放入其中) 12 ps:后面會推出一些常見的網絡安全的處理 13 14 8、性能: 一次網絡往返時間(通過數據庫查詢session信息)總比做一次HMACSHA256計算 的Token驗證和解析要費時得多. 15 16 9、不需要為登錄頁面做特殊處理: 如果你使用Protractor 做功能測試的時候,不再需要為登錄頁面做特殊處理. 17 18 10、基於標准化:你的API可以采用標准化的 JSON Web Token (JWT). 這個標准已經存在多個后端庫(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).
2、基礎性知識和demo搭建參考問題1所提的博客就可以了。話不多說進入主題,學習如下內容需要對JWT有簡單的了解。
3、在使用JWT的時候,你會發現token的生命周期不能夠進行續命。此處有不少小伙伴可能不理解什么意思。
我舉個栗子,用戶A進入一個系統,獲取到一個token,這個token默認有效期是30分鍾,但是由於是業務系統,幾乎工作時間都在使用這個系統。那么等到30分鍾token到期的時候,用戶還是需要重新登錄。
期望:用戶不斷操作給token續命,如果token還未失效,只要用戶處於活躍狀態就一直給token順延到期時間,即當前時間向后推遲30分鍾,如果token已經失效,則重新登錄。
現狀:JWT由於無法更新失效時間(主要原因token的產生和失效時間有關系),當用戶登錄系統沒到30分鍾就需要重新登錄一次,無論用戶是否活躍。
4、針對這個問題,網友也各抒己見。
1) 用redis存儲jwt(這個可以實現,但是有違jwt的初衷)
2) 利用refreshToken來刷新JWT (這個方案個人覺得意義不大)
5、下面我們直接開始擼代碼。
1、添加JWT依賴。
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency>
2、創建JWT測試代碼類。
import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.AlgorithmMismatchException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.DecodedJWT; import net.sf.json.JSONObject; import org.apache.commons.codec.binary.Base64; import java.io.UnsupportedEncodingException; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * @author LH * @version V1.0 * @Package com.purang.test * @date 2021/3/25 16:59 * @description: 修改描述 */ public class JwtUtils { //SING簽名的設置,不能對外暴露(可以提取到配置文件) public static final String SING = "token!J1JK3JH^&g%f*f@f*(f!)fs*#s*$H3J4DK43"; //產生token的有效時長,單位分鍾(可以提取到配置文件) public static final int TOKEN_EFFECTIVE_TIME_MINUTE = 1; //回話連接的有效時長,單位分鍾(可以提取到配置文件) public static final int SCOKET_EFFECTIVE_TIME_MINUTE = 140; public static void main(String[] args) throws UnsupportedEncodingException { /* //1、模擬第一次登錄獲取token Map<String,String> tokenMap = new HashMap<>();
tokenMap.put("userName","張三zhangsan1");
tokenMap.put("passWord","張三zhangsan1pass");
String token = getToken(tokenMap);*/
//================================================ //2、模擬用戶操作系統校驗token,有效則直接獲取所需信息,直接訪問,其他異常做相應處理 String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzV29yZCI6IuW8oOS4iXpoYW5nc2FuMXBhc3MiLCJ1c2VyTmFtZSI6IuW8oOS4iXpoYW5nc2FuMSIsImV4cCI6MTYxNjcyMjI0MH0.T6lm9kQjzUpJlio-MT2arAhAciFMzpdHyVQtpdkFjjA"; JSONObject tokenPayloadJson = null; try { //根據前台請求頭中獲取token的Base64編碼值 String[] tokenSplit = token.split("\\."); //解析載荷信息,用於用戶信息的續命 tokenPayloadJson = decodeBase64(tokenSplit[1]); System.out.println("舊token=="+token); System.out.println("舊tokenStr=="+tokenPayloadJson.toString()); //校驗token合法性 verify(token); System.out.println("有效簽名,驗證通過"); } catch (SignatureVerificationException e) { e.printStackTrace(); System.out.println("無效簽名"); } catch (TokenExpiredException e) { e.printStackTrace(); //只有在token過期的時候才可以進行token重生策略的調用 System.out.println("token已過期!"); //因JWT只精確到秒,所以此處獲取的時間戳你會發現少了后面三位(也就是毫秒) Long token_time = tokenPayloadJson.getLong("exp"); Date date = new Date(); //token已經超期多少時間 System.out.println("token已經超期的時間="+(date.getTime()/1000 - token_time)/60+"分鍾"); //token創建時間點到現在的時間 int time = (int) ((date.getTime()/1000 - token_time)/60 - TOKEN_EFFECTIVE_TIME_MINUTE); System.out.println("token創建時間點到現在的時間="+time+"分鍾"); if(time < SCOKET_EFFECTIVE_TIME_MINUTE){ System.out.println(">>>>>>>>>>>>>>>>>>token可以重新產生"); Map<String,String> map = new HashMap<>(); tokenPayloadJson.forEach((k, v) -> { if(!"exp".equals((String)k)) map.put((String)k, (String)v); }); map.put("newToken","new"+Math.random()); String newToken = getToken(map); System.out.println("新token=="+newToken); System.out.println("新tokenStr=="+decodeBase64(getTokenInfo(newToken).getPayload())); } } catch (AlgorithmMismatchException e) { e.printStackTrace(); System.out.println("token算法不一致!"); } catch (Exception e) { e.printStackTrace(); System.out.println("token無效!!!"); } } /** * @Description 生成token header.payload.sing * @Author LH * @Date 2021-03-26 10:48:52 * @Param Map 集合存入payload信息 * @Return token * @Exception */ public static String getToken(Map<String, String> map) { //設置令牌的過期時間 Calendar instance = Calendar.getInstance(); //設置失效時間 instance.add(Calendar.MINUTE, TOKEN_EFFECTIVE_TIME_MINUTE); //創建JWT builder JWTCreator.Builder builder = JWT.create(); //payload map.forEach((k, v) -> { builder.withClaim(k, v); }); System.out.println("時間的times"+instance.getTime().getTime()); String token = builder.withExpiresAt(instance.getTime()) //指定令牌過期時間 .sign(Algorithm.HMAC256(SING)); return token; } /** * @Description 驗證令牌是否合法 * @Author LH * @Date 2021-03-26 10:57:04 */ public static void verify(String token) { JWT.require(Algorithm.HMAC256(SING)).build().verify(token); } /** * @Author LH * @Date 2021-03-26 11:13:52 * @Return DecodedJWT JWT信息獲取 * @Description 獲取JWT的信息 */ public static DecodedJWT getTokenInfo(String token) { DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING)).build().verify(token); return verify; } /** * @Author LH * @Date 2021-03-26 11:13:52 * @Return 解析TOKEN后的JSONObject內容 * @Description 根據Base64獲取解析內容 */ public static JSONObject decodeBase64(String tokenPayload) throws UnsupportedEncodingException { byte[] bytes = Base64.decodeBase64(tokenPayload); String decode = new String(bytes, "UTF-8"); return JSONObject.fromObject(decode); } }
3、新建token,運行結果如下
打開如下代碼注釋:
//1、模擬第一次登錄獲取token
Map<String,String> tokenMap = new HashMap<>();
tokenMap.put("userName","張三zhangsan1");
tokenMap.put("passWord","張三zhangsan1pass");
String token = getToken(tokenMap);
========================================================================
時間的times1616731158511 舊token==eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzV29yZCI6IuW8oOS4iXpoYW5nc2FuMXBhc3MiLCJ1c2VyTmFtZSI6IuW8oOS4iXpoYW5nc2FuMSIsImV4cCI6MTYxNjczMTE1OH0.tf7dT_kHFAto-AJ8BzCYI8YY8pmmgY8javepTNwGZHY 舊tokenStr=={"passWord":"張三zhangsan1pass","userName":"張三zhangsan1","exp":1616731158} 有效簽名,驗證通過
4、校驗token有效期,進行動態token續命。
舊token==eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzV29yZCI6IuW8oOS4iXpoYW5nc2FuMXBhc3MiLCJ1c2VyTmFtZSI6IuW8oOS4iXpoYW5nc2FuMSIsImV4cCI6MTYxNjczMTE1OH0.tf7dT_kHFAto-AJ8BzCYI8YY8pmmgY8javepTNwGZHY 舊tokenStr=={"passWord":"張三zhangsan1pass","userName":"張三zhangsan1","exp":1616731158} com.auth0.jwt.exceptions.TokenExpiredException: The Token has expired on Fri Mar 26 11:59:18 CST 2021. at com.auth0.jwt.JWTVerifier.assertDateIsFuture(JWTVerifier.java:379) at com.auth0.jwt.JWTVerifier.assertValidDateClaim(JWTVerifier.java:370) at com.auth0.jwt.JWTVerifier.verifyClaims(JWTVerifier.java:295) at com.auth0.jwt.JWTVerifier.verify(JWTVerifier.java:278) at com.auth0.jwt.JWTVerifier.verify(JWTVerifier.java:261) at com.purang.track.testCase.JwtUtils.verify(JwtUtils.java:131) at com.purang.track.testCase.JwtUtils.main(JwtUtils.java:55) token已過期! token已經超期的時間=88分鍾 token創建時間點到現在的時間=87分鍾 >>>>>>>>>>>>>>>>>>token可以重新產生 時間的times1616736521241 新token==eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzV29yZCI6IuW8oOS4iXpoYW5nc2FuMXBhc3MiLCJuZXdUb2tlbiI6Im5ldzAuMjEzNTkzMjYwNTMxNDY3IiwidXNlck5hbWUiOiLlvKDkuIl6aGFuZ3NhbjEiLCJleHAiOjE2MTY3MzY1MjF9.r3HaEzSRwgY4UlBFNtVX39vUAC0_drIZhWL4j4tZOVc 新tokenStr=={"passWord":"張三zhangsan1pass","newToken":"new0.213593260531467","userName":"張三zhangsan1","exp":1616736521}
5、有了新的token就可以進行前台返回了,后面請求就會拿新token進行驗證了。