fix: 登录验证错误信息传递修复

- JwtTokenProvider 新增 validateTokenWithReason 方法,返回具体错误原因
- JwtAuthenticationFilter token 验证失败时返回 401 错误响应
- 前端 axios 拦截器增强 403 处理,区分 token 过期和权限不足场景

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
En 2026-03-16 15:26:37 +08:00
parent f25664cf9a
commit 57f9c804c9
4 changed files with 139 additions and 6 deletions

View File

@ -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. 登录时输入错误密码 → 显示具体错误信息

View File

@ -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('请求的资源不存在');

View File

@ -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<String, Object> 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 ")) {

View File

@ -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) {