创建获取用户接口与 JWT 认证器
适用模块:
ai-springboot-new(心理健康助手后端·新版)涉及包:
controller、config、util技术栈:Spring Boot 4.1.1 + Spring Security 6.x + Auth0 java-jwt 4.4.0 + MyBatis-Plus 3.5.17
关联文档:《JWT 认证与 Token 创建方法实现》、《引入 Spring Security 安全框架与授权规则配置》
1. 背景与目标
在《JWT 认证与 Token 创建方法实现》中,已经具备了 Token 的签发能力 (JwtTokenUtil.generateToken)和授权规则 (SecurityConfig 的白名单 + 兜底认证)。但当时 SecurityConfig 尚未挂载任何 JWT 过滤器,导致除白名单外所有接口一律返回 403------签发出来的 Token 没有任何消费方去验证它。
本次新增/改动补齐了认证链路的三个关键拼图:
| 目标 | 实现方式 | 本次状态 |
|---|---|---|
| 获取当前用户接口 | UserController.getCurrentUser() 暴露 /api/user/current |
✅ 接口已建,逻辑待补 |
| JWT 认证过滤器骨架 | JwtAuthticationFilter 继承 OncePerRequestFilter |
✅ 已挂载,仅做日志 |
| 过滤器接入安全链 | SecurityConfig 注册 Bean 并以 addFilterBefore 挂载 |
✅ 已落地 |
| 白名单复用 | SecurityConfig.isPublicPath() 供过滤器跳过公开路径 |
✅ 已落地 |
| 过滤器内验签与写入上下文 | 提取 Token → 验签 → 回查用户 → 写入 SecurityContext |
❌ 尚未实现(仅打印日志) |
说明:本次为 JWT 认证器的骨架版本 。
JwtAuthticationFilter目前只打印请求 URI/方法,并未真正解析 Token 或写入SecurityContext。因此认证链路已"接通管道"但还没"通水",详见第 6 节。
2. 本次改动文件一览
| 文件路径 | 类型 | 说明 |
|---|---|---|
src/main/java/com/example/aispringbootnew/util/JwtAuthticationFilter.java |
新增 | JWT 认证过滤器(本次核心,当前为骨架) |
src/main/java/com/example/aispringbootnew/config/SecurityConfig.java |
修改 | 注册过滤器 Bean、挂载到安全链、新增 isPublicPath |
src/main/java/com/example/aispringbootnew/controller/UserController.java |
修改 | 新增获取当前用户接口 /api/user/current |
src/main/java/com/example/aispringbootnew/util/JwtTokenUtil.java |
已有 | Token 签发工具(本次未改动,作为过滤器后续依赖) |
src/main/java/com/example/aispringbootnew/config/JwtConfig.java |
已有 | JWT 配置属性绑定(过滤器后续需读取 header/tokenPrefix) |
3. 获取当前用户接口:UserController.getCurrentUser
3.1 接口定义
38:42:src/main/java/com/example/aispringbootnew/controller/UserController.java
@GetMapping("/current")
public Result<UserLoginResponseDTO.UserDetailResponseDTO> getCurrentUser() {
// 从token中解析出用户id
return null;
}
接口元信息:
| 项 | 值 |
|---|---|
| 路径 | GET /api/user/current |
| 入参 | 无(身份信息后续从请求头携带的 Token 解析) |
| 出参 | Result<UserLoginResponseDTO.UserDetailResponseDTO> |
| 认证要求 | 需要认证 (不在 PUBLIC_PATHS 白名单内,受 anyRequest().authenticated() 兜底保护) |
该接口是登录态的"回显"入口:前端拿到 Token 后,凭此接口换取当前登录用户的基本信息,无需反复携带用户名/密码。
3.2 当前状态与预期实现
方法体目前直接 return null,仅留注释"从 token 中解析出用户 id",表明逻辑待补。
完成后的预期实现(当前尚未落地):
java
@GetMapping("/current")
public Result<UserLoginResponseDTO.UserDetailResponseDTO> getCurrentUser() {
// 1. 从 SecurityContext 取出过滤器写入的身份(userId)
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 2. 回查用户明细
UserLoginResponseDTO.UserDetailResponseDTO user =
userService.getUserById(Long.valueOf(authentication.getName()));
return Result.success(user);
}
由于该接口不在白名单中,在过滤器真正完成"验签 + 写入 SecurityContext"之前,访问它会因 SecurityContext 无身份而触发 403 或 NPE。因此它依赖第 5 节过滤器闭环后才能正常工作。
4. JWT 认证过滤器:JwtAuthticationFilter
4.1 类结构与跳过策略
12:27:src/main/java/com/example/aispringbootnew/util/JwtAuthticationFilter.java
public class JwtAuthticationFilter extends OncePerRequestFilter {
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String requestURI = request.getRequestURI();
return SecurityConfig.isPublicPath(requestURI);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取请求的URL和方法
String requestURI = request.getRequestURI();
System.out.printf("requestURI: %s\n", requestURI);
String requestMethod = request.getMethod();
System.out.printf("requestMethod: %s\n", requestMethod);
}
}
两点设计要点:
- 继承
OncePerRequestFilter:保证一次请求只执行一次过滤逻辑,不会被FORWARD/INCLUDE等内部转发重复触发------这是自定义认证过滤器的标准基类等。 shouldNotFilter白名单跳过 :通过SecurityConfig.isPublicPath(requestURI)判断当前路径是否为公开路径。命中白名单(登录、注册、健康检查、根路径)时返回true,Spring 直接跳过该过滤器,避免对公开接口做无意义的 Token 校验。
4.2 白名单复用的闭环
SecurityConfig 中 isPublicPath 与授权规则共用同一份 PUBLIC_PATHS 数组:
22:37:src/main/java/com/example/aispringbootnew/config/SecurityConfig.java
public static final String[] PUBLIC_PATHS = {
"/",
"/api/test",
"/api/user/login",
"/api/user/add"
};
public static boolean isPublicPath(String requestURI) {
for (String publicPath : PUBLIC_PATHS) {
if (antPathMatcher.match(publicPath, requestURI)) {
return true;
}
}
return false;
}
两处共用同一数组,是"白名单只维护一处"的关键设计:
- 授权阶段 :
requestMatchers(PUBLIC_PATHS).permitAll()放行; - 认证阶段 :
shouldNotFilter跳过过滤器,不要求 Token。
二者一致,避免"授权放行但过滤器仍索要 Token"的矛盾。使用 Hutool 的 AntPathMatcher 做匹配,为后续 /api/** 这类通配路径留有余地。
4.3 当前过滤逻辑(仅日志)
doFilterInternal 目前仅打印请求 URI 与方法名,既不提取 Token,也不写入 SecurityContext,也没有调用 filterChain.doFilter 放行请求:
21:27:src/main/java/com/example/aispringbootnew/util/JwtAuthticationFilter.java
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取请求的URL和方法
String requestURI = request.getRequestURI();
System.out.printf("requestURI: %s\n", requestURI);
String requestMethod = request.getMethod();
System.out.printf("requestMethod: %s\n", requestMethod);
}
⚠️ 注意两个隐患:
- 方法体内没有
filterChain.doFilter(request, response)------一旦后续移除shouldNotFilter的跳过逻辑(或访问非白名单路径),请求会被"卡死"在过滤器中、永远到不了 Controller。- 使用
System.out.printf调试输出,应替换为日志框架(log)。
5. 过滤器接入安全链:SecurityConfig 改动
5.1 注册过滤器 Bean
39:42:src/main/java/com/example/aispringbootnew/config/SecurityConfig.java
@Bean
public JwtAuthticationFilter jwtAuthticationFilter() {
return new JwtAuthticationFilter();
}
将过滤器声明为 Spring Bean,使其可被安全链引用、并享受容器管理。
5.2 挂载到过滤器链
44:61:src/main/java/com/example/aispringbootnew/config/SecurityConfig.java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(PUBLIC_PATHS).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
插入点选择 UsernamePasswordAuthenticationFilter 之前 :该位置早于 AuthorizationFilter,能保证授权判定(anyRequest().authenticated())执行前,用户身份已经被写入 SecurityContext。若插在其后,授权阶段读到的一定是匿名用户,所有受保护接口都会 403。
整体链路(本次已打通管道):
HTTP 请求
│
▼
Servlet 容器过滤器链
│
▼
Spring Security FilterChainProxy
│ ├─ CsrfFilter(已禁用)
│ ├─ ★ JwtAuthticationFilter ← 本次挂载(shouldNotFilter 跳过白名单;非白名单仅打印日志)
│ ├─ UsernamePasswordAuthenticationFilter(表单登录,本项目不使用)
│ ├─ AuthorizationFilter ← 执行 authorizeHttpRequests 授权规则
│ └─ ...
▼
DispatcherServlet → Controller(/api/user/current 等受保护接口)
6. 认证链路时序(预期 vs 现状)
完整认证应在过滤器内完成 ① 提取 Token → ② 验签解析 → ③ 回查用户状态 → ④ 写入 SecurityContext → ⑤ 放行。当前 JwtAuthticationFilter 仅实现到第 0 步(日志),对照如下:
| 步骤 | 预期行为 | 本次实现 |
|---|---|---|
| 跳过白名单 | shouldNotFilter 命中即放行 |
✅ 已实现 |
| ① 提取 Token | 从 Authorization 头读取并剥离 Bearer 前缀 |
❌ 未实现 |
| ② 验签解析 | JwtTokenUtil.validateToken 校验签名/签发者/过期,解析 userId/username/roleType |
❌ 未实现 |
| ③ 状态回查 | UserService.getUserById 确认用户存在且为 NORMAL |
❌ 未实现 |
| ④ 写入上下文 | 构造 UsernamePasswordAuthenticationToken 写入 SecurityContextHolder |
❌ 未实现 |
| ⑤ 放行 | filterChain.doFilter(request, response) |
❌ 未实现(方法内缺失该调用) |
也就是说,当前状态下访问
/api/user/current:过滤器跳过白名单为 false → 进入doFilterInternal仅打印日志 → 由于没有doFilter调用,请求不会继续 。这恰恰暴露了骨架必须补全filterChain.doFilter才能放行。在补齐 ①②③④⑤ 之前,受保护接口实际不可达。
7. 配置清单(与过滤器相关的待消费项)
JwtConfig/application.yml 中已存在、但当前过滤器尚未使用的配置:
13:19:src/main/resources/application.yml
# JWT配置
jwt:
secret: MySecretKeyForJWT2025!@#$%^&*()_+SecureKeyHere
expiration: 86400000 # 24小时(毫秒)
refresh-expiration: 604800000 # 7天(毫秒)
header: Authorization # token头部名称
token-prefix: "Bearer " # token前缀
| 配置项 | 过滤器是否使用 | 说明 |
|---|---|---|
jwt.header |
❌ 未使用 | 提取 Token 时应读取该头名,而非硬编码 |
jwt.token-prefix |
❌ 未使用 | 提取 Token 时应剥离 Bearer 前缀 |
jwt.secret |
❌ 未使用(验签阶段需要) | 验签时用作 HMAC256 校验密钥 |
jwt.expiration |
❌ 未使用(签发阶段已用) | 验签时 exp 由 Token 自身携带 |
8. 调用示例
8.1 访问获取当前用户接口(预期,待闭环)
请求:
http
GET /api/user/current HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
预期响应:
json
{
"code": "200",
"msg": "操作成功",
"data": { "id": 1, "username": "zhangsan", "userType": "1" }
}
当前实际:过滤器未放行(缺
doFilter),请求卡在过滤器;即使放行,因SecurityContext无身份,getCurrentUser返回null。需补齐第 6 节 ①②③④⑤ 后才可达预期。
8.2 过滤器日志输出(当前真实行为)
启动后访问任意接口,控制台会打印(白名单路径会被 shouldNotFilter 直接跳过、不进 doFilterInternal,故不打印):
text
requestURI: /api/user/current
requestMethod: GET
9. 现存问题与风险
按严重程度排列,建议优先处理前两项。
① doFilterInternal 缺少放行调用,请求会被卡死(致命)
21:27:src/main/java/com/example/aispringbootnew/util/JwtAuthticationFilter.java
protected void doFilterInternal(HttpServletRequest request, ...) {
String requestURI = request.getRequestURI();
System.out.printf("requestURI: %s\n", requestURI);
String requestMethod = request.getMethod();
System.out.printf("requestMethod: %s\n", requestMethod);
// ← 缺少 filterChain.doFilter(request, response);
}
过滤器的职责是"处理后再放行"。当前方法体未调用 filterChain.doFilter(...),所有进入 doFilterInternal 的请求(即非白名单路径)都会永远停在此处,无法到达 Controller,表现为接口无响应/超时。这是骨架必须立即补上的最小改动。
修复(至少保证放行,后续在放行前补验签):
java
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// TODO: 提取 Token → 验签 → 写入 SecurityContext
filterChain.doFilter(request, response);
}
② 过滤器未真正验签,认证形同虚设(高)
当前 doFilterInternal 既不读取 Token,也不写入 SecurityContext,等于"所有进来的请求都无身份放行(一旦补上 doFilter)"。anyRequest().authenticated() 的兜底授权在其后执行时永远读到匿名用户,受保护接口实际无法被合法用户访问、也无法被拦截 。getCurrentUser 中的 SecurityContext 也永远为空。
必须补齐:从请求头提取 Token → JwtTokenUtil.validateToken 验签 → 回查用户状态 → 构造 UsernamePasswordAuthenticationToken 写入 SecurityContextHolder,才完成认证闭环。
③ 调试输出应使用日志框架(低)
System.out.printf 应替换为 SLF4J 的 log,避免生产环境噪音且利于日志采集。
④ 白名单未包含 /api/user/current(提示)
PUBLIC_PATHS 目前为 /、/api/test、/api/user/login、/api/user/add,不含 /api/user/current------这符合预期(获取当前用户本就需要认证)。此处仅为提示:若误把该接口加入白名单,认证将失去意义。
⑤ 依赖 JwtTokenUtil 的验签方法(待实现)
第 ② 步所需的 validateToken / extractTokenFromRequest 在 ai-springboot-new 的 JwtTokenUtil 中目前只有 generateToken ,尚无验签与提取方法(可参考旧版 ai-spingboot 项目的同名工具类实现)。过滤器闭环前需先补齐这两个静态方法。
10. 后续演进建议
- 修复第 9 节问题 ① :在
doFilterInternal末尾补上filterChain.doFilter(request, response),保证请求至少能放行(最小可运行版本)。 - 补齐验签侧 :在
JwtTokenUtil新增extractTokenFromRequest(request)(按jwt.header读取并剥离jwt.tokenPrefix)与validateToken(token)(校验签名/iss/exp,返回解析结果)。 - 完成过滤器认证主流程 :提取 Token → 验签 → 命中
UserService.getUserById回查状态(NORMAL)→ 构造UsernamePasswordAuthenticationToken写入SecurityContextHolder;失败分支通过ResponseUtil.writeError直接写回 JSON(过滤器早于DispatcherServlet,@RestControllerAdvice不生效),并在写错后return中断链。 - 实现
getCurrentUser业务 :从SecurityContext取身份 → 回查用户明细 →Result.success(...),替换当前return null。 - 密钥外部化 :
jwt.secret明文入库风险高,建议改为secret: ${JWT_SECRET}环境变量注入。 - 方法级授权 :
@EnableMethodSecurity已开启,为管理员接口补充@PreAuthorize("hasRole('ADMIN')")。
11. 涉及文件清单
| 文件路径 | 类型 | 说明 |
|---|---|---|
src/main/java/com/example/aispringbootnew/util/JwtAuthticationFilter.java |
新增 | JWT 认证过滤器骨架(本次核心),当前仅日志、未验签 |
src/main/java/com/example/aispringbootnew/config/SecurityConfig.java |
修改 | 注册过滤器 Bean、以 addFilterBefore 挂载、新增 isPublicPath 白名单复用 |
src/main/java/com/example/aispringbootnew/controller/UserController.java |
修改 | 新增 /api/user/current 获取当前用户接口(逻辑待补) |
src/main/java/com/example/aispringbootnew/util/JwtTokenUtil.java |
已有 | Token 签发工具,过滤器后续依赖其 validateToken/extractTokenFromRequest(待实现) |
src/main/java/com/example/aispringbootnew/config/JwtConfig.java |
已有 | JWT 配置属性绑定(含 header/tokenPrefix,过滤器尚未消费) |
src/main/resources/application.yml |
已有 | jwt.* 配置段,提供过滤器验签所需的密钥与头信息 |