JWT+SpringBoot+SpringMVC引數解析器
大家以前都使用過session儲存資訊,有的交給容器建立,有的儲存到mysql或者redis,這次專案用到了JWT,我們把使用者的資訊和登入的過期時間都封裝到一個token字串裡,客戶端每次請求只需要在頭資訊裡攜帶token即可,話不多說,下面是目錄結構.

一.annonation註解
package com.demo.annotation; import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface IgnoreLogin { } 複製程式碼
該註解主要作用是過濾掉請求攔截器,使用該註解就不會對該請求進行攔截(許可權校驗),具體使用下面講.
package com.demo.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 登入使用者資訊 */ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface LoginUser {} 複製程式碼
該註解作用是SpringMVC引數解析器,類似於RequestBody註解(希望大家瞭解springmvc的引數解析機制),和我們後面的resolver相關聯.
二.bean實體類
package com.demo.bean; public class User { private long userId; private String userName; private String password; 忽略get/set } 複製程式碼
我們的使用者資訊
package com.demo.bean; public class Business { private String str; private int num; 忽略get/set } 複製程式碼
我們的業務引數
三.config配置資訊
package com.demo.config; import com.demo.interceptor.AuthorizationInterceptor; import com.demo.resolver.UserArgumentResolver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; /** * MVC配置 */ @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private AuthorizationInterceptor authorizationInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authorizationInterceptor).addPathPatterns("/**"); //注入我們自定義的攔截器,攔截所有請求 } @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(new UserArgumentResolver()); //注入我們的使用者引數解析器 } } 複製程式碼
四.controller
package com.demo.controller; import com.demo.annotation.IgnoreLogin; import com.demo.annotation.LoginUser; import com.demo.bean.Business; import com.demo.bean.User; import com.demo.util.JwtUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController public class UserController { private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private JwtUtils jwtUtils; @PostMapping(value = "/login") @IgnoreLogin public String login() { //在此 我們不做登入檢驗 假設檢驗成功 User user = new User(); user.setUserId(9527); user.setUserName("小星星"); return jwtUtils.generateToken(user);//這裡只是為了測試只返回token,(請求不含IgnoreLogin註解時需要將token放在頭資訊裡) } @PostMapping("/business") public User business(@RequestBody Business business, @LoginUser User user) {//在業務邏輯可以使用註解將我們的user注入進來 logger.info("使用者資訊引數id:{},姓名:{}", user.getUserId(), user.getUserName()); logger.info("我們的業務引數:{},{}", business.getStr(), business.getNum()); return user; } } 複製程式碼
可以看到當我們登陸成功後我們可以生成一個token字串返回給客戶端,這個字串包含了使用者資訊和時間資訊(jwt機制),同時我們做了一個模仿業務的請求,business是我們的業務引數,user是我們根據客戶端上發的token解析出來的,下面會講到如何解析.可以看到只要我們需要user的引數,我們就可以直接使用LoginUser註解和User就可以直接得到,非常方便,客戶端並不需要將我們的使用者資訊參雜到我們的業務引數中.相對安全。
五.exception
package com.demo.exception; public class RRException extends RuntimeException { private static final long serialVersionUID = 1L; private String msg; private int code = 500; } 複製程式碼
這裡我就不解析了,根據需求可以和客戶端協商相應的錯誤碼
六.interceptor攔截器
package com.demo.interceptor; import com.demo.annotation.IgnoreLogin; import com.demo.exception.RRException; import com.demo.util.JwtUtils; import io.jsonwebtoken.Claims; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 許可權(Token)驗證 */ @Component public class AuthorizationInterceptor extends HandlerInterceptorAdapter { @Autowired private JwtUtils jwtUtils; public static final String USER_KEY = "user"; private Logger logger = LoggerFactory.getLogger(getClass()); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod) || ((HandlerMethod) handler). getMethodAnnotation(IgnoreLogin.class) != null) { //如果不是HandlerMethod或者忽略登入 logger.info("無需token校驗,handler:{}", handler); return true; } //獲取使用者憑證 String token = request.getHeader(jwtUtils.getHeader()); if (StringUtils.isBlank(token)) { token = request.getParameter(jwtUtils.getHeader()); } //憑證為空 if (StringUtils.isBlank(token)) { throw new RRException(jwtUtils.getHeader() + "不能為空", HttpStatus.UNAUTHORIZED.value()); } Claims claims = jwtUtils.getClaimByToken(token); if (claims == null || jwtUtils.isTokenExpired(claims.getExpiration())) { throw new RRException(jwtUtils.getHeader() + "失效,請重新登入", HttpStatus.UNAUTHORIZED.value()); } //設定userId到request裡,後續根據userId,獲取使用者資訊 request.setAttribute(USER_KEY, jwtUtils.getUser(claims)); return true; } } 複製程式碼
我們會過濾掉不是HandlerMethod的請求和帶有IgnoreLogin的註解(並不是所有方法都需要校驗,例如登入請求,支付回撥請求),我們會取出客戶端發出的token,解析出來並判斷是否過期,沒有token或者已過期我們可以需要返回一個錯誤碼給客戶端然後重新登入,當我們校驗成功後我們會取出使用者資訊放入到request裡(後面會在引數解析器裡解析出來),這也是這個攔截器的精髓,既能校驗又能獲取使用者的資訊.
七.resolver引數解析器
package com.demo.resolver; import com.demo.annotation.LoginUser; import com.demo.bean.User; import com.demo.interceptor.AuthorizationInterceptor; import org.springframework.core.MethodParameter; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import javax.servlet.http.HttpServletRequest; /** * 使用者引數解析器 */ public class UserArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(LoginUser.class); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); User user = (User) request.getAttribute(AuthorizationInterceptor.USER_KEY); return user; } } 複製程式碼
springmvc的引數解析器,需要繼承HandlerMethodArgumentResolver,有兩個方法,第一個就是支援什麼型別的引數,可以看到我們支援擁有LoginUser註解的引數,第二個方法是從request裡取出我們在攔截器中放入的user並返回,這樣就實現了user物件的注入.
八.JwtUtils
package com.demo.util; import com.alibaba.fastjson.JSONObject; import com.demo.bean.User; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * jwt工具類 */ @Component @ConfigurationProperties(prefix = "jwt") public class JwtUtils { private Logger logger = LoggerFactory.getLogger(getClass()); private longexpire; private String secret; private String header; /** * 生成jwt token */ public String generateToken(User user) { Date nowDate = new Date(); //過期時間 Date expireDate = new Date(nowDate.getTime() + expire * 1000); return Jwts.builder() .setHeaderParam("typ", "JWT") .setSubject(JSONObject.toJSONString(user)) .setIssuedAt(nowDate) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } /** * 解析出來claim * @param token * @return */ public Claims getClaimByToken(String token) { try { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } catch (Exception e) { logger.debug("validate is token error ", e); return null; } } /** * 得到user * @param claims * @return */ public User getUser(Claims claims) { return JSONObject.parseObject(claims.getSubject(), User.class); } /** * token是否過期 * @return true:過期 */ public boolean isTokenExpired(Date expiration) { return expiration.before(new Date()); } public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } public long getExpire() { return expire; } public void setExpire(long expire) { this.expire = expire; } public String getHeader() { return header; } public void setHeader(String header) { this.header = header; } } 複製程式碼
expire過期時間,secret金鑰,header頭資訊名稱 這些資料在application.yml裡,這裡我們會根據User物件生成一個token字串,根據token取出claims物件,這裡就包含了我們的過期時間和之前我們所存的user資訊.
九.springboot啟動和yml引數
package com.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringBootStart { public static void main(String[] agrs) { SpringApplication.run(SpringBootStart.class, agrs); } } application.yml jwt: #加密祕鑰 secret: f4e2e5203fg45sf45g4de581c0f9eb5 #token,單位秒 expire: 6000 header: token 複製程式碼