From b9bf191c8b1896f2702a450d12583da0d8282cbe Mon Sep 17 00:00:00 2001 From: longguancheng Date: Mon, 26 Jan 2026 11:14:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=8B=A6=E6=88=AA=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../publics/advice/AuthInterceptor.java | 103 ++++++++++-------- 1 file changed, 60 insertions(+), 43 deletions(-) diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/advice/AuthInterceptor.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/advice/AuthInterceptor.java index 2f1a005..1298f3f 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/advice/AuthInterceptor.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/advice/AuthInterceptor.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.zhangy.skyeye.common.extend.anno.IgnoreAuth; import com.zhangy.skyeye.common.extend.exception.AuthException; 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.SpringUtil; 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.HttpServletResponse; 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 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 { - - // 1. 放行 OPTIONS 请求(CORS 预检请求),让过滤器在响应时添加cors头 - if (HttpMethod.OPTIONS.name().equals(request.getMethod())) { + // 放行 CORS 预检请求(OPTIONS),由后续 Filter 统一处理跨域头 + if (HttpMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) { return true; } - - // 2. 静态资源直接放行 + // 放行静态资源请求 if (handler instanceof ResourceHttpRequestHandler) { return true; } - - // 3. 非 HandlerMethod 的请求(如默认处理器)拒绝访问 + // 只处理 HandlerMethod(Controller 方法),其他情况一律拒绝 if (!(handler instanceof HandlerMethod)) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied"); return false; } - - HandlerMethod handlerMethod = (HandlerMethod)handler; + HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); - if (method.getAnnotation(IgnoreAuth.class) != null) { + // 带有 @IgnoreAuth 注解的方法直接放行(无需登录) + if (method.isAnnotationPresent(IgnoreAuth.class)) { return true; } - + // 获取并校验 token String token = SecurityUtil.getToken(request); - if (token == null) { - throw new AuthException("请求未授权"); + if (token == null || token.trim().isEmpty()) { + throw new AuthException("请求未授权:缺少 token"); } - - // 测试 - if (ObjectUtil.isNotEmpty(token)) { - if (token.equals("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ayJleHAiOjE2NjMyOTQwMzQsInVzZXJuYW1lIjoie1widXNlcklkXCI6MSxcInVzZXJuYW1lXCI6XCJhZG1pblwifSJ9.aFM87d8iLyRUyHwfd8Lt0a7BDNxIyLAZaVgkiJ6cjHg")) { - return true; - } - } - - String userName = JwtUtil.getUserName(token); - if (userName == null) { - throw new AuthException("请求未授权"); - } - - // 校验token是否失效 + // 解析 token 中的用户名信息 + String usernamePayload; try { - UserDTO userDTO = JsonUtil.parseEx(userName, UserDTO.class); - String redisKeyUserToken = userDTO.getTokenKey(); - Object redisToken = redisUtil.get(redisKeyUserToken); - if(redisToken == null){ - throw new AuthException("登录状态已失效,请重新登陆"); - } - if(!token.equals(redisToken)){ - throw new AuthException("该账号在另一台设备登录,请重新登陆"); - } - redisUtil.expire(redisKeyUserToken, JwtUtil.EXPIRE_TIME / 1000); - } catch (JsonProcessingException e) { - log.error("token解析异常", e); - throw new AuthException("请求未授权,请重新登录"); + usernamePayload = JwtUtil.getUserName(token); + } catch (Exception e) { + log.warn("JWT 解析失败,token: {}", token.substring(0, Math.min(20, token.length())), e); + throw new AuthException("无效的 token,请重新登录"); } - //log.debug("请求来源:" + request.getRemoteAddr()); + if (usernamePayload == null || usernamePayload.trim().isEmpty()) { + throw new AuthException("token 内容异常,请重新登录"); + } + // 尝试将 payload 解析为 UserDTO + UserDTO userDTO; + try { + userDTO = JsonUtil.parseEx(usernamePayload, UserDTO.class); + } catch (JsonProcessingException e) { + log.error("token 中的用户信息解析失败: {}", usernamePayload, e); + throw new AuthException("token 格式错误,请重新登录"); + } + // 获取该用户在 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; } }