本篇和大家分享jwt(json web token)的使用,她主要用來生成介面訪問的token和驗證,其單獨結合springboot來開發api介面token驗證很是方便,由於jwt的token中儲存有使用者的資訊並且有加密,所以適用於分散式,這樣直接吧資訊儲存在使用者本地減速了服務端儲存sessiion或token的壓力;如下快速使用:
1 <!--jwt--> 2 <dependency> 3 <groupId>io.jsonwebtoken</groupId> 4 <artifactId>jjwt</artifactId> 5 <version>0.9.0</version> 6 </dependency> 7 <!--阿里 FastJson依賴--> 8 <dependency> 9 <groupId>com.alibaba</groupId> 10 <artifactId>fastjson</artifactId> 11 <version>1.2.44</version> 12 </dependency>
一般使用jwt來達到3種結果:
- 生成token
- 驗證token是否有效
- 獲取token中jwt資訊(主要使用者資訊)
生成token
引入了jjwt依賴後,要生成token很方便;對於一個token來說,代表的是唯一併且不可逆的,因此我們在生成時需要增加一些唯一資料進去,比如下面的id:
1 long currentTime = System.currentTimeMillis(); 2 return Jwts.builder() 3 .setId(UUID.randomUUID().toString()) 4 .setIssuedAt(new Date(currentTime)) //簽發時間 5 .setSubject("system") //說明 6 .setIssuer("shenniu003") //簽發者資訊 7 .setAudience("custom") //接收使用者 8 .compressWith(CompressionCodecs.GZIP) //資料壓縮方式 9 10 .signWith(SignatureAlgorithm.HS256, encryKey) //加密方式 11 .setExpiration(new Date(currentTime + secondTimeOut * 1000)) //過期時間戳 12 .addClaims(claimMaps) //cla資訊 13 .compact();
通過uuid來標記唯一id資訊;當然在對token加密時需要用到祕鑰,jwt很是方便她支援了很多中加密方式如:HS256,HS265,Md5等複雜及常用的加密方式;
jwt生成的token中內容分為3個部分:head資訊,payload資訊,sign資訊,通常我們要做的是往payload增加一些使用者資訊(比如:賬號,暱稱,許可權等,但不包含密碼);在對jwt的token有一定了解後,我們來看下真實生成的token值:
1 eyJhbGciOiJIUzI1NiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAAFWMTQ7CIBSE7_LWkPDzaEsP4QnYINCIptX4INE0vbtg4sLlfPPN7HAtGWbwg1BKL4GrcbEcIwpujZF8iiEpjXFapAAG2ReYpUEcR2VxYED13Nb0ppLW3hP1eEnblqsQuiFfY0OhUrl3I70evweU_aFSejZhd7DlcDv5NTmYHUilHTD3rf_hAccHRTv--7YAAAA.i4xwoQtaWI0-dwHWN8uZ4DBm-vfli5bavYU9lRYxU5E
驗證token是否有效
token生成的時都會伴隨者有一個失效的時間,在這我們可以通過setExpiration函式設定過期時間,記住jwt的有效時間不是滑動的,也就是說不做任何處理時,當到達第一次設定的失效時間時,就基本沒用了,要獲取token是否過期可以使用如下方式:
1 public static boolean isExpiration(String token, String encryKey) { 2 try { 3 return getClaimsBody(token, encryKey) 4 .getExpiration() 5 .before(new Date()); 6 } catch (ExpiredJwtException ex) { 7 return true; 8 } 9 }
這裡使用了date的before來用獲取的過期時間和當前時間對比,判斷是否繼續有效,需要注意的是如果在token失效後再通過getClaimsBody(token, encryKey)獲取資訊,此時會報ExpiredJwtException錯誤,我們即可認為過期。
獲取token中jwt資訊(主要使用者資訊)
通常我們要把登入使用者資訊儲存在jwt生成的token中,這裡可以通過 addClaims(claimMaps) 傳遞map來設定資訊,反過來要獲取token中的使用者資訊,我們需要這樣做:
1 return Jwts.parser() 2 .setSigningKey(encryKey) 3 .parseClaimsJws(token) 4 .getBody();
此時body獲取出來是Claims型別,我們需要從中獲取到使用者資訊,需要注意的是在addClaims儲存資訊的時候如果儲存的map值沒做過出來,那完整的實體物件儲存進去後會對映成一個LinkHasMap型別,如下:
因此通常會在儲存的時候json化,如下程式碼:
1 claimMaps.forEach((key, val) -> { 2 claimMaps.put(key, JSON.toJSONString(val)); 3 });
再來就是通過get方法獲取我們儲存進去的資訊,並json反序列化:
1 /** 2 * 獲取body某個值 3 * 4 * @param token 5 * @param encryKey 6 * @param key 7 * @return 8 */ 9 public static Object getVal(String token, String encryKey, String key) { 10 return getJws(token, encryKey).getBody().get(key); 11 } 12 13 /** 14 * 獲取body某個值,json字元轉實體 15 * 16 * @param token 17 * @param encryKey 18 * @param key 19 * @param tClass 20 * @param <T> 21 * @return 22 */ 23 public static <T> T getValByT(String token, String encryKey, String key, Class<T> tClass) { 24 try { 25 String strJson = getVal(token, encryKey, key).toString(); 26 return JSON.parseObject(strJson, tClass); 27 } catch (Exception ex) { 28 return null; 29 } 30 }
來到這裡一個Jwt的Util程式碼基本就完成了,下面給出完整的程式碼例子,僅供參考:
1 public class JwtUtil { 2 3 /** 4 * 獲取token - json化 map資訊 5 * 6 * @param claimMaps 7 * @param encryKey 8 * @param secondTimeOut 9 * @return 10 */ 11 public static String getTokenByJson(Map<String, Object> claimMaps, String encryKey, int secondTimeOut) { 12 return getToken(claimMaps, true, encryKey, secondTimeOut); 13 } 14 15 /** 16 * 獲取token 17 * 18 * @param claimMaps 19 * @param isJsonMpas 20 * @param encryKey 21 * @param secondTimeOut 22 * @return 23 */ 24 public static String getToken(Map<String, Object> claimMaps, boolean isJsonMpas, String encryKey, int secondTimeOut) { 25 26 if (isJsonMpas) { 27 claimMaps.forEach((key, val) -> { 28 claimMaps.put(key, JSON.toJSONString(val)); 29 }); 30 } 31 long currentTime = System.currentTimeMillis(); 32 return Jwts.builder() 33 .setId(UUID.randomUUID().toString()) 34 .setIssuedAt(new Date(currentTime)) //簽發時間 35 .setSubject("system") //說明 36 .setIssuer("shenniu003") //簽發者資訊 37 .setAudience("custom") //接收使用者 38 .compressWith(CompressionCodecs.GZIP) //資料壓縮方式 39 40 .signWith(SignatureAlgorithm.HS256, encryKey) //加密方式 41 .setExpiration(new Date(currentTime + secondTimeOut * 1000)) //過期時間戳 42 .addClaims(claimMaps) //cla資訊 43 .compact(); 44 } 45 46 /** 47 * 獲取token中的claims資訊 48 * 49 * @param token 50 * @param encryKey 51 * @return 52 */ 53 private static Jws<Claims> getJws(String token, String encryKey) { 54 return Jwts.parser() 55 .setSigningKey(encryKey) 56 .parseClaimsJws(token); 57 } 58 59 public static String getSignature(String token, String encryKey) { 60 try { 61 return getJws(token, encryKey).getSignature(); 62 } catch (Exception ex) { 63 return ""; 64 } 65 } 66 67 /** 68 * 獲取token中head資訊 69 * 70 * @param token 71 * @param encryKey 72 * @return 73 */ 74 public static JwsHeader getHeader(String token, String encryKey) { 75 try { 76 return getJws(token, encryKey).getHeader(); 77 } catch (Exception ex) { 78 return null; 79 } 80 } 81 82 /** 83 * 獲取payload body資訊 84 * 85 * @param token 86 * @param encryKey 87 * @return 88 */ 89 public static Claims getClaimsBody(String token, String encryKey) { 90 return getJws(token, encryKey).getBody(); 91 } 92 93 /** 94 * 獲取body某個值 95 * 96 * @param token 97 * @param encryKey 98 * @param key 99 * @return 100 */ 101 public static Object getVal(String token, String encryKey, String key) { 102 return getJws(token, encryKey).getBody().get(key); 103 } 104 105 /** 106 * 獲取body某個值,json字元轉實體 107 * 108 * @param token 109 * @param encryKey 110 * @param key 111 * @param tClass 112 * @param <T> 113 * @return 114 */ 115 public static <T> T getValByT(String token, String encryKey, String key, Class<T> tClass) { 116 try { 117 String strJson = getVal(token, encryKey, key).toString(); 118 return JSON.parseObject(strJson, tClass); 119 } catch (Exception ex) { 120 return null; 121 } 122 } 123 124 /** 125 * 是否過期 126 * 127 * @param token 128 * @param encryKey 129 * @return 130 */ 131 public static boolean isExpiration(String token, String encryKey) { 132 try { 133 return getClaimsBody(token, encryKey) 134 .getExpiration() 135 .before(new Date()); 136 } catch (ExpiredJwtException ex) { 137 return true; 138 } 139 } 140 141 public static String getSubject(String token, String encryKey) { 142 try { 143 return getClaimsBody(token, encryKey).getSubject(); 144 } catch (Exception ex) { 145 return ""; 146 } 147 } 148 }
過濾器驗證token
有了基本的JwtUtil工具,我們需要用到springboot專案中,一般來說對於登入授權token驗證可以通過過濾器來操作,這裡建立一個AuthenFilter,用於對post請求過來的token做驗證:
1 public class AuthenFilter implements Filter { 2 @Override 3 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 4 5 HttpServletRequest rq = (HttpServletRequest) servletRequest; 6 HttpServletResponse rp = (HttpServletResponse) servletResponse; 7 RpBase rpBase = new RpBase(); 8 try { 9 //只接受post 10 if (!rq.getMethod().equalsIgnoreCase("post")) { 11 filterChain.doFilter(servletRequest, servletResponse); 12 return; 13 } 14 15 String token = rq.getHeader("token"); 16 if (StringUtils.isEmpty(token)) { 17 rpBase.setMsg("無token"); 18 return; 19 } 20 21 //jwt驗證 22 MoUser moUser = JwtUtil.getValByT(token, WebConfig.Token_EncryKey, WebConfig.Login_User, MoUser.class); 23 if (moUser == null) { 24 rpBase.setMsg("token已失效"); 25 return; 26 } 27 28 System.out.println("token使用者:" + moUser.getNickName()); 29 30 filterChain.doFilter(servletRequest, servletResponse); 31 } catch (Exception ex) { 32 } finally { 33 if (!StringUtils.isEmpty(rpBase.getMsg())) { 34 rp.setCharacterEncoding("utf-8"); 35 rpBase.setCode(HttpStatus.BAD_REQUEST.value()); 36 rp.getWriter().write(JSON.toJSONString(rpBase)); 37 } 38 } 39 } 40 }
要是自定義過濾器AuthenFilter生效,還需要把她註冊到容器中,這裡通過編碼方式,當然還可以通過@WebFilter註解來加入到容器中:
1 @Configuration 2 public class WebFilterConfig { 3 4 @Bean 5 public FilterRegistrationBean setFilter() { 6 7 FilterRegistrationBean registrationBean = new FilterRegistrationBean(); 8 registrationBean.setFilter(new AuthenFilter()); 9 registrationBean.addUrlPatterns("/api/*"); 10 registrationBean.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE); 11 12 return registrationBean; 13 } 14 }
注意addUrlPatterns匹配的是過濾器作用的url連線,根據需求而定;為了驗證效果,這裡我建立了兩個介面getToken和t0,分別是獲取token和post查詢介面,程式碼如是:
1 @RestController 2 public class TestController { 3 4 @PostMapping("/api/t0") 5 public String t0() throws MyException { 6 7 return UUID.randomUUID().toString(); 8 } 9 10 @GetMapping("/token/{userName}") 11 public String getToken(@PathVariable String userName) { 12 13 MoUser moUser = new MoUser(); 14 moUser.setUserName(userName); 15 moUser.setNickName(userName); 16 17 Map<String, Object> map = new HashMap<>(); 18 map.put(WebConfig.Login_User, moUser); 19 20 return JwtUtil.getTokenByJson(map, 21 WebConfig.Token_EncryKey, 22 WebConfig.Token_SecondTimeOut); 23 } 24 }
最終要獲通過head傳遞token值來訪問t01介面,得到如下結果:
token在有效時間後訪問直接失敗,從新獲取token並訪問t01介面,得到成功的資訊: