fix: 登录验证错误信息传递修复
- JwtTokenProvider 新增 validateTokenWithReason 方法,返回具体错误原因 - JwtAuthenticationFilter token 验证失败时返回 401 错误响应 - 前端 axios 拦截器增强 403 处理,区分 token 过期和权限不足场景 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f25664cf9a
commit
57f9c804c9
55
docs/dev-logs/2026-03-16.md
Normal file
55
docs/dev-logs/2026-03-16.md
Normal 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. 登录时输入错误密码 → 显示具体错误信息
|
||||
@ -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('请求的资源不存在');
|
||||
|
||||
@ -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 ")) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user