springboot+jwt做api的token認證

神牛003發表於2018-12-15

本篇和大家分享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 }
View Code

過濾器驗證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介面,得到成功的資訊:

 

相關文章