目录
项目结构
一、 引入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.6</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>
二、自定义Auth认证注解
在需要认证的方法上添加该注解
package com.dev.jwt;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 定义一个名为Auth的注解,用于标注需要授权的方法或类型。
*
* @Target 指定该注解可以应用于方法或类型的元素上。
* @Retention 指定该注解在运行时是可见的。
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
/**
* 是否需要授权的属性。
* 默认值为true,表示该方法或类型需要授权才能访问。
* 设置为false时表示不需要授权。
*/
boolean require() default true;
}
三、 编写登录拦截器
通过识别是否在接口上添加@Auth注解来确定是否需要登录才能访问。
同时这里需要注意只拦截HandlerMethod类型,同时还要考虑放行BasicErrorController,因为基本的报错在这个控制器中,如果不放行,那么会看不到报错信息。
package com.dev.jwt;
import org.apache.commons.lang.StringUtils;
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 登录拦截器类,实现 HandlerInterceptor 接口,用于拦截请求并进行登录验证。
*/
public class LoginInterceptor implements HandlerInterceptor {
/**
* Spring MVC 的 HandlerInterceptor 接口的实现类,用于处理请求的前置拦截。
* 主要功能是进行权限验证,如果请求需要认证,且认证失败,则重定向到错误页面。
*
* @param request HTTP 请求对象
* @param response HTTP 响应对象
* @param handler 处理请求的处理器对象,可能是 Controller 方法或其他类型的处理器
* @return 是否继续处理请求,如果返回 false,则不会继续执行该请求的处理器方法
* @throws Exception 如果处理过程中抛出异常
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断处理器是否为 HandlerMethod 类型,即是否为一个方法处理器
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 检查处理器方法所属的 bean 是否为 BasicErrorController 类型,如果是,则直接放行
if (handlerMethod.getBean() instanceof BasicErrorController) {
return true;
}
// 获取处理器方法上的 Auth 注解,用于判断该方法是否需要认证
Auth auth = handlerMethod.getMethod().getAnnotation(Auth.class);
// 如果存在 Auth 注解且要求认证
if (auth != null && auth.require()) {
// 从请求头中获取 token,用于认证
String token = request.getHeader("token");
// 如果 token 不为空且通过验证
if (StringUtils.isNotBlank(token)) {
if (TokenUtil.verifyToken(token)) {
// 认证成功,继续处理请求
return true;
} else {
// 认证失败,重定向到 token 错误页面
request.getRequestDispatcher("/error/tokenError").forward(request, response);
}
} else {
// 未提供 token,重定向到 token 缺失页面
request.getRequestDispatcher("/error/token").forward(request, response);
}
} else {
// 不需要认证,继续处理请求
return true;
}
} else {
// 非 HandlerMethod 类型的处理器,直接放行
return true;
}
// 如果执行到此处,表示拦截处理失败,不应继续处理请求
return false;
}
}
四、定义跨域拦截器
这里是做前后端分离需要做的步骤,解决跨域的方式有好几种,这里使用拦截器的方式解决跨域问题。
package com.dev.jwt;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 跨域拦截器类,用于处理跨域请求。
* 实现HandlerInterceptor接口,提供预处理请求的能力。
*/
public class CrossInterceptorHandler implements HandlerInterceptor {
/**
* 在请求处理之前进行预处理。
* 主要用于设置响应头,以允许来自特定源的跨域请求。
*
* @param request HttpServletRequest对象,代表客户端的请求。
* @param response HttpServletResponse对象,用于向客户端发送响应。
* @param handler 将要处理请求的具体处理器对象。
* @return 返回true表示继续处理请求,返回false表示中断请求处理。
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 设置响应头,允许来自https://example.com的跨域请求
response.setHeader("Access-Control-Allow-Origin", "https://example.com");
// 允许浏览器发送cookie
response.setHeader("Access-Control-Allow-Credentials", "true");
// 允许的HTTP方法
response.setHeader("Access-Control-Allow-Methods", "POST, GET , PUT , OPTIONS");
// 预检请求的缓存时间
response.setHeader("Access-Control-Max-Age", "3600");
// 允许的请求头
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,accept,authorization,content-type");
// 返回true,表示继续后续的处理流程
return true;
}
}
五、 定义全局异常处理器
为了项目的完整性,将这些常规的内容写上去。
package com.dev.jwt.handler;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.dev.jwt.model.emum.ResponseEnum;
import com.dev.jwt.utils.R;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器,用于捕获并处理应用程序中抛出的特定异常。
* 使用@RestControllerAdvice注解,标识这个类是一个处理全局异常的控制器顾问。
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 日志记录器,用于记录异常信息。
*/
public final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 处理Token过期异常。
* 当用户持有的Token过期时,此异常会被抛出。
*
* @param e Token过期异常的具体实例,包含详细的错误信息。
* @return 返回一个封装了错误信息的响应对象。
*/
@ExceptionHandler(TokenExpiredException.class)
public R<?> handleTokenExpiredException(TokenExpiredException e) {
// 记录token过期的错误信息
logger.error("token 已过期");
logger.error(e.getMessage());
// 返回一个表示Token过期的错误响应
return R.error(ResponseEnum.TOKEN_EX);
}
}
六、定义工具类
1. 统一错误状态码
编写一个枚举类,统一项目的报错状态码
@AllArgsConstructor
@Getter
public enum ResponseEnum {
SUCCESS(200, "操作成功"),
FAIL(300,"获取数据失败"),
USER_EX(301,"用户不存在,请重新登录"),
ERROR(302,"错误请求"),
USERNAME_PASSWORD_ERROR(303,"用户名或密码错误"),
NO_TOKEN(400,"无token,请重新登录"),
TOKEN_VERIFY_ERROR(401,"token验证失败,请重新登录"),
TOKEN_EX(402,"token已过期");
private final Integer code;
private final String msg;
public static ResponseEnum getResultCode(Integer code){
for (ResponseEnum value : ResponseEnum.values()) {
if (code.equals(value.getCode())){
return value;
}
}
return ResponseEnum.ERROR;
}
}
2.统一响应类
@Data
public class R<T> implements Serializable {
private static final long serialVersionUID = 56665257244236049L;
private Integer code;
private String message;
private T data;
private R() {
}
public static <T> R<T> ok(T data) {
R<T> response = new R<>();
response.setCode(ResponseEnum.SUCCESS.getCode());
response.setMessage(ResponseEnum.SUCCESS.getMsg());
response.setData(data);
return response;
}
public static <T> R<T> error(Integer errCode, String errMessage) {
R<T> response = new R<>();
response.setCode(errCode);
response.setMessage(errMessage);
return response;
}
public static <T> R<T> error(ResponseEnum responseEnum) {
R<T> response = new R<>();
response.setCode(responseEnum.getCode());
response.setMessage(responseEnum.getMsg());
return response;
}
}
3.Token工具类
通过TokenUtil可以生成token和验证token是否正确。
package com.dev.jwt;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Date;
/**
* Token工具类,用于生成和验证JWT令牌。
*/
public class TokenUtil {
/**
* JWT加密的密钥。
*/
private final static String ENCRYPT_KEY = "abc123";
/**
* JWT令牌的过期时间,单位为分钟。
*/
private final static int EXPIRE_TIME = 1;
/**
* JWT令牌的发行者。
*/
private static final String ISSUER = "zhangsan";
/**
* 生成JWT令牌。
*
* @param json 令牌中承载的信息,以JSONObject形式提供。
* @return 生成的JWT令牌字符串。
*/
public static String createToken(JSONObject json) {
// 使用JWT创建一个令牌
return JWT.create()
// 设置令牌的主题,即json对象转换后的字符串 不要把密码封装进去,不安全
.withSubject(json.toString())
// 设置令牌的发行者
.withIssuer(ISSUER)
// 设置令牌的过期时间,以当前时间为基础加上设定的过期时间
.withExpiresAt(DateUtil.offsetMinute(new Date(), EXPIRE_TIME))
// 设置自定义的声明,这里以"test"为键,"123"为值
.withClaim("test", "123")
// 使用HMAC256算法对令牌进行签名加密
.sign(Algorithm.HMAC256(ENCRYPT_KEY));
}
/**
* 验证JWT令牌的有效性。
*
* @param token 待验证的JWT令牌字符串。
* @return 如果令牌有效,则返回true;否则返回false。
*/
public static boolean verifyToken(String token) {
try {
// 创建一个 JWT 验证器,使用 HMAC256 算法,密钥为 ENCRYPT_KEY,发行者为 ISSUER
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(ENCRYPT_KEY))
.withIssuer(ISSUER)
.build();
// 对令牌进行验证
jwtVerifier.verify(token);
// 如果验证成功,返回 true
return true;
} catch (Exception e) {
// 验证失败,打印异常信息,并返回false
e.printStackTrace();
return false;
}
}
}
七、 编写实体类
这里为了简单,并没有与数据库交互。
@Data
public class User {
private String userName;
private String password;
private String token;
}
八、 定义控制器
1.定义登录控制器类
@RestController
@RequestMapping("/user")
public class LoginController {
@PostMapping("/login")
public R<User> login(String userName, String password) {
if (StringUtils.isNotBlank(userName) && StringUtils.isNotBlank(password)) {
if ("张三".equals(userName) && "123456".equals(password)) {
User user = new User();
JSONObject json = JSONUtil.createObj()
.put("name", "zhangsan");
String token = TokenUtil.createToken(json);
user.setToken(token);
return R.ok(user);
}
}
return R.error(ResponseEnum.USERNAME_PASSWORD_ERROR);
}
}
2 定义报错处理器
@RestController
@RequestMapping("/error")
public class ErrorController {
@PostMapping("/token")
public R<?> token() {
return R.error(ResponseEnum.NO_TOKEN);
}
@PostMapping("/tokenError")
public R<?> tokenError() {
return R.error(ResponseEnum.TOKEN_VERIFY_ERROR);
}
}
3 定义测试控制器
@RestController
@RequestMapping("/test")
public class TestController {
@Auth
@PostMapping("/hello")
public R<?> hello() {
return R.ok("登录成功");
}
@PostMapping("/hi")
public R<?> hi() {
return R.ok("登录成功");
}
}
九、 配置类
将自定义的两个拦截器注册进去。
package com.dev.jwt.config;
import com.dev.jwt.interceptor.CrossInterceptorHandler;
import com.dev.jwt.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* WebMvcConfigurer的实现类,用于自定义Spring MVC的配置,例如拦截器的设置。
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 添加拦截器到应用中。
*
* @param registry 拦截器注册表,用于注册和管理拦截器。
*
* 本方法中,首先添加了一个处理跨域请求的拦截器CrossInterceptorHandler,应用到所有路径。
* 接着添加了一个登录拦截器LoginInterceptor,应用到所有路径,但排除了/user/login和/error/**路径。
* 这样配置是为了确保登录页面和错误页面不受登录拦截器的影响。
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CrossInterceptorHandler()).addPathPatterns(new String[] {"/**"});
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**").excludePathPatterns("/user/login", "/error/**");
}
}