JWT token验证后,通过 ThreadLocal 进行传值
代码结构如图:需要源码联系QQ:47262947
1. 添加 Maven 引用
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.0</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.0</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred --> <version>0.11.0</version> <scope>runtime</scope> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.3.7</version> </dependency>
2. 添加 annotation JwtIgnore 用于忽略不需要验证的接口
package com.vipsoft.web.boot.annotation; import java.lang.annotation.*; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface JwtIgnore { }
3. 添加工具类 JwtUtils
package com.vipsoft.web.boot.utils; 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; @ConfigurationProperties(prefix = "vipsoft.jwt") @Component public class JwtUtils { private Logger logger = LoggerFactory.getLogger(getClass()); private String secret; private long expire; private String header; /** * 生成jwt token */ public String generateToken(long userId) { Date nowDate = new Date(); //过期时间 Date expireDate = new Date(nowDate.getTime() + expire * 1000); return Jwts.builder() .setHeaderParam("typ", "JWT") .setSubject(userId+"") //这边不加“”的话,取的时候可能会报过期 .setIssuedAt(nowDate) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } 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; } } /** * 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; } }
4. 添加拦截器 AuthorizationInterceptor
package com.vipsoft.web.boot.interceptor; import cn.hutool.core.util.StrUtil; import com.vipsoft.web.boot.annotation.JwtIgnore; import com.vipsoft.web.boot.exception.CustomException; import com.vipsoft.web.boot.utils.JwtUtils; import io.jsonwebtoken.Claims; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; 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; @Component public class AuthorizationInterceptor extends HandlerInterceptorAdapter { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private JwtUtils jwtUtils; public static final String USER_KEY = "userId"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 忽略带JwtIgnore注解的请求, 不做后续token认证校验 if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; JwtIgnore jwtIgnore = handlerMethod.getMethodAnnotation(JwtIgnore.class); if (jwtIgnore != null) { return true; } } if (HttpMethod.OPTIONS.equals(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); return true; } //获取用户凭证 String token = request.getHeader(jwtUtils.getHeader()); if (StrUtil.isBlank(token)) { token = request.getParameter(jwtUtils.getHeader()); } //凭证为空 if (StrUtil.isBlank(token)) { throw new CustomException(HttpStatus.UNAUTHORIZED.value(), "token 不能为空"); } Claims claims = jwtUtils.getClaimByToken(token); if (claims == null || jwtUtils.isTokenExpired(claims.getExpiration())) { throw new CustomException(HttpStatus.UNAUTHORIZED.value(), "token 失效,请重新登录"); } //设置userId到request里,后续根据userId,获取用户信息 request.setAttribute(USER_KEY, Long.parseLong(claims.getSubject())); return true; } }
5. 添加 WebConfig,启用拦截器
package com.vipsoft.web.boot.config; import com.vipsoft.web.boot.interceptor.AuthorizationInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private AuthorizationInterceptor authorizationInterceptor; /** * 添加拦截器 */ @Override public void addInterceptors(InterceptorRegistry registry) { //拦截路径可自行配置多个 可用 ,分隔开 registry.addInterceptor(authorizationInterceptor).addPathPatterns("/**"); } /** * 跨域支持 * * @param registry */ @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowCredentials(true) .allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD") .maxAge(3600 * 24); } }
6. 添加返回对象 Result
package com.vipsoft.web.boot.utils; /** * 返回的对象,项目中要移到Core中 */ public class Result<T> { //操作代码 int code; //提示信息 String message; T Data; public Result(int code, String message){ this.code = code; this.message = message; } public Result(int code, String message,T data){ this.code = code; this.message = message; this.setData(data); } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getData() { return Data; } public void setData(T data) { Data = data; } }
7. 添加自定义异常类 CustomException.java 用于拦截器中的异常捕获
package com.vipsoft.web.boot.exception; public class CustomException extends RuntimeException { private static final long serialVersionUID = 1L; private int code; private String msg; public CustomException(String msg) { super(msg); this.code = 500; this.msg = msg; } public CustomException(String msg, Throwable e) { super(msg, e); this.msg = msg; } public CustomException(int code, String msg) { super(msg); this.code = code; this.msg = msg; } public CustomException(int code, String msg, Throwable e) { super(msg, e); this.code = code; this.msg = msg; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } }
8. 添加全局异常处理类 GlobalExceptionHandler.java , 可以将 返回 前端的数据处理成 JSON格式
package com.vipsoft.web.boot.exception; import com.vipsoft.web.boot.utils.Result; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.List; /** * 全局异常处理器 */ @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); /** * 处理自定义异常 */ @ExceptionHandler(CustomException.class) public Result handleException(CustomException e) { // 打印异常信息 logger.error("### 异常信息:{} ###", e.getMessage()); return new Result(e.getCode(), e.getMessage()); } /** * 参数错误异常 */ @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class}) public Result handleException(Exception e) { if (e instanceof MethodArgumentNotValidException) { MethodArgumentNotValidException validException = (MethodArgumentNotValidException) e; BindingResult result = validException.getBindingResult(); StringBuffer errorMsg = new StringBuffer(); if (result.hasErrors()) { List<ObjectError> errors = result.getAllErrors(); errors.forEach(p -> { FieldError fieldError = (FieldError) p; errorMsg.append(fieldError.getDefaultMessage()).append(","); logger.error("### 请求参数错误:{" + fieldError.getObjectName() + "},field{" + fieldError.getField() + "},errorMessage{" + fieldError.getDefaultMessage() + "}"); }); } } else if (e instanceof BindException) { BindException bindException = (BindException) e; if (bindException.hasErrors()) { logger.error("### 请求参数错误: {}", bindException.getAllErrors()); } } return new Result(10001, "参数无效"); } /** * 处理所有不可知的异常 */ @ExceptionHandler(Exception.class) public Result handleOtherException(Exception e) { //打印异常堆栈信息 e.printStackTrace(); // 打印异常信息 logger.error("### 不可知的异常:{} ###", e.getMessage()); return new Result(40001, "系统内部错误"); } }
9. 配置 application.yml
vipsoft: jwt: # 加密秘钥 secret: d3d3LnZpcHNvZnQuY29tLmNuLjQ3MjYyOTQ3LnNwcmluZyBib290 # token有效时长,单位秒 expire: 60 # 方便测试,设成 60 秒 header: token
10. 登录测试代码
/** * 登录 */ @JwtIgnore @GetMapping("login") public Map<String, Object> login(HttpServletRequest request){ //用户登录 long userId = 47262947; //生成token String token = jwtUtils.generateToken(userId); Map<String, Object> map = new HashMap<>(); map.put("token", token); map.put("expire", jwtUtils.getExpire()); return map; }