修改拦截器

This commit is contained in:
longguancheng 2026-01-26 11:14:27 +08:00
parent e7e12bc6a1
commit b9bf191c8b

View File

@ -4,7 +4,6 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.zhangy.skyeye.common.extend.anno.IgnoreAuth; import com.zhangy.skyeye.common.extend.anno.IgnoreAuth;
import com.zhangy.skyeye.common.extend.exception.AuthException; import com.zhangy.skyeye.common.extend.exception.AuthException;
import com.zhangy.skyeye.common.extend.util.JsonUtil; import com.zhangy.skyeye.common.extend.util.JsonUtil;
import com.zhangy.skyeye.common.extend.util.ObjectUtil;
import com.zhangy.skyeye.common.utils.JwtUtil; import com.zhangy.skyeye.common.utils.JwtUtil;
import com.zhangy.skyeye.common.utils.SpringUtil; import com.zhangy.skyeye.common.utils.SpringUtil;
import com.zhangy.skyeye.redis.utils.RedisUtil; import com.zhangy.skyeye.redis.utils.RedisUtil;
@ -19,70 +18,88 @@ import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Objects;
/**
* JWT + Redis 双重校验的权限拦截器
* 主要功能
* 1. 放行OPTIONS预检请求
* 2. 放行静态资源
* 3. 跳过标注了 @IgnoreAuth 的接口
* 4. 校验 JWT token 的合法性
* 5. 校验 token 是否在 Redis 中存在实现单点登录/踢出其他设备登录
* 6. 续期 Redis 中的 token 有效期
*/
@Slf4j @Slf4j
public class AuthInterceptor implements AsyncHandlerInterceptor { public class AuthInterceptor implements AsyncHandlerInterceptor {
private RedisUtil redisUtil = SpringUtil.getBean(RedisUtil.class); private final RedisUtil redisUtil = SpringUtil.getBean(RedisUtil.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 放行 CORS 预检请求OPTIONS由后续 Filter 统一处理跨域头
// 1. 放行 OPTIONS 请求CORS 预检请求让过滤器在响应时添加cors头 if (HttpMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) {
if (HttpMethod.OPTIONS.name().equals(request.getMethod())) {
return true; return true;
} }
// 放行静态资源请求
// 2. 静态资源直接放行
if (handler instanceof ResourceHttpRequestHandler) { if (handler instanceof ResourceHttpRequestHandler) {
return true; return true;
} }
// 只处理 HandlerMethodController 方法其他情况一律拒绝
// 3. HandlerMethod 的请求如默认处理器拒绝访问
if (!(handler instanceof HandlerMethod)) { if (!(handler instanceof HandlerMethod)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied"); response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
return false; return false;
} }
HandlerMethod handlerMethod = (HandlerMethod) handler;
HandlerMethod handlerMethod = (HandlerMethod)handler;
Method method = handlerMethod.getMethod(); Method method = handlerMethod.getMethod();
if (method.getAnnotation(IgnoreAuth.class) != null) { // 带有 @IgnoreAuth 注解的方法直接放行无需登录
if (method.isAnnotationPresent(IgnoreAuth.class)) {
return true; return true;
} }
// 获取并校验 token
String token = SecurityUtil.getToken(request); String token = SecurityUtil.getToken(request);
if (token == null) { if (token == null || token.trim().isEmpty()) {
throw new AuthException("请求未授权"); throw new AuthException("请求未授权:缺少 token");
} }
// 解析 token 中的用户名信息
// 测试 String usernamePayload;
if (ObjectUtil.isNotEmpty(token)) {
if (token.equals("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ayJleHAiOjE2NjMyOTQwMzQsInVzZXJuYW1lIjoie1widXNlcklkXCI6MSxcInVzZXJuYW1lXCI6XCJhZG1pblwifSJ9.aFM87d8iLyRUyHwfd8Lt0a7BDNxIyLAZaVgkiJ6cjHg")) {
return true;
}
}
String userName = JwtUtil.getUserName(token);
if (userName == null) {
throw new AuthException("请求未授权");
}
// 校验token是否失效
try { try {
UserDTO userDTO = JsonUtil.parseEx(userName, UserDTO.class); usernamePayload = JwtUtil.getUserName(token);
String redisKeyUserToken = userDTO.getTokenKey(); } catch (Exception e) {
Object redisToken = redisUtil.get(redisKeyUserToken); log.warn("JWT 解析失败token: {}", token.substring(0, Math.min(20, token.length())), e);
if(redisToken == null){ throw new AuthException("无效的 token请重新登录");
throw new AuthException("登录状态已失效,请重新登陆");
} }
if(!token.equals(redisToken)){ if (usernamePayload == null || usernamePayload.trim().isEmpty()) {
throw new AuthException("该账号在另一台设备登录,请重新登陆"); throw new AuthException("token 内容异常,请重新登录");
} }
redisUtil.expire(redisKeyUserToken, JwtUtil.EXPIRE_TIME / 1000); // 尝试将 payload 解析为 UserDTO
UserDTO userDTO;
try {
userDTO = JsonUtil.parseEx(usernamePayload, UserDTO.class);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
log.error("token解析异常", e); log.error("token 中的用户信息解析失败: {}", usernamePayload, e);
throw new AuthException("请求未授权,请重新登录"); throw new AuthException("token 格式错误,请重新登录");
} }
//log.debug("请求来源:" + request.getRemoteAddr()); // 获取该用户在 Redis 中存储的 token
String redisKey = userDTO.getTokenKey();
if (redisKey == null || redisKey.trim().isEmpty()) {
throw new AuthException("用户信息异常,请重新登录");
}
Object storedToken = redisUtil.get(redisKey);
// Redis 中不存在 登录已失效
if (storedToken == null) {
throw new AuthException("登录状态已失效,请重新登录");
}
// Redis 中的 token 与当前请求不一致 多设备登录被踢
if (!Objects.equals(token, storedToken.toString())) {
log.warn("检测到多端登录冲突,用户: {}, 当前token: {}, redisToken: {}",
usernamePayload, token.substring(0, 10) + "...",
storedToken.toString().substring(0, 10) + "...");
throw new AuthException("该账号已在其他设备登录,请重新登录");
}
// 校验通过 续期 Redis token 有效时间
redisUtil.expire(redisKey, JwtUtil.EXPIRE_TIME / 1000);
return true; return true;
} }
} }