From 57f9c804c91f5db3ebc685a0599ba4ff8594d712 Mon Sep 17 00:00:00 2001 From: En Date: Mon, 16 Mar 2026 15:26:37 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=99=BB=E5=BD=95=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E4=BF=A1=E6=81=AF=E4=BC=A0=E9=80=92=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JwtTokenProvider 新增 validateTokenWithReason 方法,返回具体错误原因 - JwtAuthenticationFilter token 验证失败时返回 401 错误响应 - 前端 axios 拦截器增强 403 处理,区分 token 过期和权限不足场景 Co-Authored-By: Claude Opus 4.6 --- docs/dev-logs/2026-03-16.md | 55 +++++++++++++++++++ reading-platform-frontend/src/api/index.ts | 18 +++++- .../security/JwtAuthenticationFilter.java | 50 ++++++++++++++++- .../common/security/JwtTokenProvider.java | 22 +++++++- 4 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 docs/dev-logs/2026-03-16.md diff --git a/docs/dev-logs/2026-03-16.md b/docs/dev-logs/2026-03-16.md new file mode 100644 index 0000000..4777895 --- /dev/null +++ b/docs/dev-logs/2026-03-16.md @@ -0,0 +1,55 @@ +# 2026-03-16 开发日志 + +## 修复内容 + +### 登录验证错误信息传递修复 + +**问题描述:** +- 后端的 `JwtTokenProvider.validateToken()` 方法没有把错误信息返回给前端 +- 前端对 403 错误没有完善处理 + +**修复内容:** + +#### 1. 后端修改 + +**文件:** `reading-platform-java/src/main/java/com/reading/platform/common/security/JwtTokenProvider.java` + +- 新增 `validateTokenWithReason(String token)` 方法,返回具体的错误原因: + - `TOKEN_EXPIRED` - token 已过期 + - `TOKEN_INVALID` - token 格式错误或无效 + - `TOKEN_UNSUPPORTED` - 不支持的 token 格式 + - `TOKEN_EMPTY` - token 为空 + +- 修改原有的 `validateToken(String token)` 方法,内部调用新方法实现 + +**文件:** `reading-platform-java/src/main/java/com/reading/platform/common/security/JwtAuthenticationFilter.java` + +- 修改 `doFilterInternal()` 方法: + - 当 token 验证失败时,不再继续执行 `filterChain.doFilter()` + - 直接返回 401 状态码和错误信息 + - 新增 `getErrorMessage()` 方法,将错误原因转换为用户友好的消息 + - 新增 `sendError()` 方法,发送 JSON 格式的错误响应 + +#### 2. 前端修改 + +**文件:** `reading-platform-frontend/src/api/index.ts` + +- 增强 403 响应拦截器处理逻辑: + - 区分 token 过期/无效导致的 403 和权限不足的 403 + - 当响应体中的错误码为 401 或 403 时,视为 token 问题,清除本地存储并跳转登录页 + - 其他情况视为权限不足,仅显示提示但不跳转 + +**修改后的错误处理逻辑:** +``` +401 → 清除存储 + 跳转登录页 +403 + 错误码 401/403 → 清除存储 + 跳转登录页(token 问题) +403 + 其他错误码 → 显示提示,不跳转(权限不足) +``` + +### 测试验证 + +后续需要验证以下场景: +1. 使用有效 token 正常访问 → 应该成功 +2. 使用过期 token 访问 → 后端返回 401 + "Token 已过期",前端跳转登录页 +3. 使用无效 token 访问 → 后端返回 401 + "Token 无效",前端跳转登录页 +4. 登录时输入错误密码 → 显示具体错误信息 diff --git a/reading-platform-frontend/src/api/index.ts b/reading-platform-frontend/src/api/index.ts index dfea31a..32f7eea 100644 --- a/reading-platform-frontend/src/api/index.ts +++ b/reading-platform-frontend/src/api/index.ts @@ -51,7 +51,23 @@ request.interceptors.response.use( window.location.href = '/login'; break; case 403: - message.error('没有权限访问'); + // 区分 token 过期/无效和权限不足的场景 + // 如果是 token 问题导致的 403,跳转到登录页 + if (data && typeof data === 'object' && 'code' in data) { + const errorCode = data.code; + // token 过期或无效时跳转到登录页 + if (errorCode === 401 || errorCode === 403) { + message.error(data.message || '登录已过期,请重新登录'); + localStorage.removeItem('token'); + localStorage.removeItem('user'); + localStorage.removeItem('role'); + localStorage.removeItem('name'); + window.location.href = '/login'; + break; + } + } + // 其他情况视为权限不足,显示提示但不跳转 + message.error(data?.message || '没有权限访问'); break; case 404: message.error('请求的资源不存在'); diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/security/JwtAuthenticationFilter.java b/reading-platform-java/src/main/java/com/reading/platform/common/security/JwtAuthenticationFilter.java index 7c8a276..81f9c46 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/common/security/JwtAuthenticationFilter.java +++ b/reading-platform-java/src/main/java/com/reading/platform/common/security/JwtAuthenticationFilter.java @@ -1,11 +1,13 @@ package com.reading.platform.common.security; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; @@ -15,6 +17,8 @@ import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; /** * JWT Authentication Filter @@ -32,13 +36,21 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { FilterChain filterChain) throws ServletException, IOException { try { String token = resolveToken(request); - if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) { + if (StringUtils.hasText(token)) { + // 验证 token 并获取错误原因 + String tokenErrorReason = jwtTokenProvider.validateTokenWithReason(token); + if (tokenErrorReason != null) { + // token 无效,返回 401 错误 + sendError(response, HttpStatus.UNAUTHORIZED, getErrorMessage(tokenErrorReason)); + return; + } + JwtPayload payload = jwtTokenProvider.getPayloadFromToken(token); // 使用 Redis 验证 token 是否有效(检查黑名单和 token 一致性) if (!jwtTokenRedisService.validateToken(payload.getUsername(), token)) { - log.debug("Token validation failed for user: {}", payload.getUsername()); - filterChain.doFilter(request, response); + log.debug("Token validation failed in Redis for user: {}", payload.getUsername()); + sendError(response, HttpStatus.UNAUTHORIZED, "Token 已失效,请重新登录"); return; } @@ -53,11 +65,43 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { } } catch (Exception e) { log.error("Cannot set user authentication: {}", e.getMessage()); + sendError(response, HttpStatus.UNAUTHORIZED, "Token 验证失败,请重新登录"); } filterChain.doFilter(request, response); } + /** + * 根据错误原因返回错误消息 + */ + private String getErrorMessage(String errorReason) { + switch (errorReason) { + case "TOKEN_EXPIRED": + return "Token 已过期,请重新登录"; + case "TOKEN_INVALID": + return "Token 无效,请重新登录"; + case "TOKEN_UNSUPPORTED": + return "不支持的 Token 格式"; + case "TOKEN_EMPTY": + return "Token 为空"; + default: + return "Token 验证失败,请重新登录"; + } + } + + /** + * 发送错误响应 + */ + private void sendError(HttpServletResponse response, HttpStatus status, String message) throws IOException { + response.setStatus(status.value()); + response.setContentType("application/json;charset=UTF-8"); + Map body = new HashMap<>(); + body.put("code", status.value()); + body.put("message", message); + String json = new ObjectMapper().writeValueAsString(body); + response.getWriter().write(json); + } + private String resolveToken(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/security/JwtTokenProvider.java b/reading-platform-java/src/main/java/com/reading/platform/common/security/JwtTokenProvider.java index a337dd3..7eb82e1 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/common/security/JwtTokenProvider.java +++ b/reading-platform-java/src/main/java/com/reading/platform/common/security/JwtTokenProvider.java @@ -71,25 +71,43 @@ public class JwtTokenProvider { .build(); } + /** + * 验证 token 是否有效 + * @param token token 字符串 + * @return true-有效,false-无效 + */ public boolean validateToken(String token) { + return validateTokenWithReason(token) == null; + } + + /** + * 验证 token 并返回错误原因 + * @param token token 字符串 + * @return 错误原因字符串,如果 token 有效则返回 null + */ + public String validateTokenWithReason(String token) { try { Jwts.parser() .verifyWith(secretKey) .build() .parseSignedClaims(token); - return true; + return null; // token 有效 } catch (MalformedJwtException e) { log.error("Invalid JWT token: {}", e.getMessage()); + return "TOKEN_INVALID"; } catch (ExpiredJwtException e) { log.error("Expired JWT token: {}", e.getMessage()); + return "TOKEN_EXPIRED"; } catch (UnsupportedJwtException e) { log.error("Unsupported JWT token: {}", e.getMessage()); + return "TOKEN_UNSUPPORTED"; } catch (IllegalArgumentException e) { log.error("JWT claims string is empty: {}", e.getMessage()); + return "TOKEN_EMPTY"; } catch (Exception e) { log.error("JWT validation error: {}", e.getMessage()); + return "TOKEN_INVALID"; } - return false; } public String getUsernameFromToken(String token) {