JWT token验证后,通过 ThreadLocal 进行传值
https://jwt.io/#debugger 官网提供的 JAVA 工具还是挺多的,选了个 Star 比较多的
https://github.com/jwtk/jjwt
代码结构如图:需要源码联系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;
}