15.创建获取用户接口与JWT认证器

创建获取用户接口与 JWT 认证器

适用模块:ai-springboot-new(心理健康助手后端·新版)

涉及包:controllerconfigutil

技术栈: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);
    }
}

两点设计要点:

  1. 继承 OncePerRequestFilter :保证一次请求只执行一次过滤逻辑,不会被 FORWARD / INCLUDE 等内部转发重复触发------这是自定义认证过滤器的标准基类等。
  2. shouldNotFilter 白名单跳过 :通过 SecurityConfig.isPublicPath(requestURI) 判断当前路径是否为公开路径。命中白名单(登录、注册、健康检查、根路径)时返回 true,Spring 直接跳过该过滤器,避免对公开接口做无意义的 Token 校验。

4.2 白名单复用的闭环

SecurityConfigisPublicPath 与授权规则共用同一份 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);
}

⚠️ 注意两个隐患

  1. 方法体内没有 filterChain.doFilter(request, response) ------一旦后续移除 shouldNotFilter 的跳过逻辑(或访问非白名单路径),请求会被"卡死"在过滤器中、永远到不了 Controller。
  2. 使用 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 / extractTokenFromRequestai-springboot-newJwtTokenUtil目前只有 generateToken ,尚无验签与提取方法(可参考旧版 ai-spingboot 项目的同名工具类实现)。过滤器闭环前需先补齐这两个静态方法。


10. 后续演进建议

  1. 修复第 9 节问题 ① :在 doFilterInternal 末尾补上 filterChain.doFilter(request, response),保证请求至少能放行(最小可运行版本)。
  2. 补齐验签侧 :在 JwtTokenUtil 新增 extractTokenFromRequest(request)(按 jwt.header 读取并剥离 jwt.tokenPrefix)与 validateToken(token)(校验签名/iss/exp,返回解析结果)。
  3. 完成过滤器认证主流程 :提取 Token → 验签 → 命中 UserService.getUserById 回查状态(NORMAL)→ 构造 UsernamePasswordAuthenticationToken 写入 SecurityContextHolder;失败分支通过 ResponseUtil.writeError 直接写回 JSON(过滤器早于 DispatcherServlet@RestControllerAdvice 不生效),并在写错后 return 中断链。
  4. 实现 getCurrentUser 业务 :从 SecurityContext 取身份 → 回查用户明细 → Result.success(...),替换当前 return null
  5. 密钥外部化jwt.secret 明文入库风险高,建议改为 secret: ${JWT_SECRET} 环境变量注入。
  6. 方法级授权@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.* 配置段,提供过滤器验签所需的密钥与头信息
相关推荐
小羊没烦恼!1 小时前
Memory 记忆设计讨论:Agent Memory 的数据模型可以怎么设计
java·开发语言·前端·c++·c#
SL_staff1 小时前
战略落地的最后一公里:从目标到个人日历的自动化链路实现
java·github·全栈
用户3721574261352 小时前
Java 设置 Excel 数据验证:整数、日期、文本长度、下拉列表和时间
java
白远山2 小时前
智慧场馆解决方案软件开发实战:从架构设计到落地部署指南
java·开发语言·架构·需求分析
SL_staff2 小时前
面向业务人员的IoT概念翻译实践:JVS-IOT低代码配置原理与开发者视角解析
java·spring boot·物联网
m0_587383002 小时前
折扣卡CPS软件开发实战:从系统架构设计到上线指南
java·小程序·架构·需求分析
Zane19943 小时前
用了Optional,为什么NPE还是防不住?
java·后端
大模型码小白3 小时前
告别造假数据,直接连数据库查真实时序数据喂给 TimechoAI 大模型
java·数据库·人工智能·microsoft·架构
企业数字化笔记3 小时前
固定资产Excel批量导入怎么防重复?资产编码、重复检查和错误回执
java·后端·excel