PAYATHON 2026

Spring Security JWT 认证中 user 为 null,如何排查?

数据小陈

查询得到的 user 为 null,随后调用 user.getAuthorities() 时抛出了空指针异常。查询语句是:

UserDetails user = userRepository.findByEmail(login);

现有代码还有一处问题:validateToken() 验证失败时返回空字符串,过滤器仍会用这个字符串查询用户。调用过验证方法,不代表 JWT 已经验证成功。

先区分两条可能的故障路径

第一条是 JWT 验证失败:

Token 过期、签名不匹配或其他验证错误
→ validateToken() 捕获 JWTVerificationException
→ 返回 ""
→ findByEmail("") 查询不到用户
→ user.getAuthorities() 抛出 NullPointerException

第二条是 JWT 验证成功,但数据库中找不到对应用户:

JWT 验证成功
→ 读取 subject
→ findByEmail(subject) 返回 null
→ user.getAuthorities() 抛出 NullPointerException

生成 Token 时使用了 .withSubject(user.getEmail()),所以查询条件应当是 Token 中的邮箱。即使 JWT 验证通过,也不能证明这个邮箱对应的用户仍然存在。

目前没有提供 UserRepository、数据库记录和实际请求头,无法仅凭这些代码确定属于哪条路径。

按顺序检查验证结果和查询条件

  1. 查看失败请求是否携带 Authorization 请求头,以及是否符合 Bearer <token> 格式。
  2. 在 validateToken() 的异常分支设置断点,确认是否发生 JWTVerificationException,并查看具体异常类型。
  3. 检查验证返回的 login 是否为空,以及是否与目标用户的邮箱一致。
  4. 确认 findByEmail() 的声明、实体字段映射,以及当前应用连接的数据库中是否存在该邮箱。若邮箱存在大小写或空白差异,还需结合查询实现和数据库比较规则检查。

调试时不要把完整 JWT、签名密钥或用户凭据写入日志。

让验证失败明确中断认证

可以移除 validateToken() 中返回空字符串的处理,让过滤器接收并处理 JWTVerificationException:

public String validateToken(String token) {
    Algorithm algorithm = Algorithm.HMAC256(secret);
    return JWT.require(algorithm)
            .withIssuer("auth-api")
            .build()
            .verify(token)
            .getSubject();
}

这样,验证失败后就不会继续查询数据库。验证成功后仍需检查 subject 和用户查询结果。

在过滤器中处理无效 Token 和不存在的用户

下面的方法可以替换原过滤器中的对应方法,继续使用已有的 tokenService 和 userRepository 字段。示例沿用原代码中 findByEmail() 直接返回 UserDetails 的形式;如果实际返回 Optional,需要按其返回类型调整。

示例采用的规则是:没有 Bearer Token 时继续过滤器链;提供了 Bearer Token,但验证失败、subject 为空或用户不存在时,返回 401。

@Override
protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
) throws ServletException, IOException {
    String token = recoverToken(request);

    if (token == null) {
        filterChain.doFilter(request, response);
        return;
    }

    if (token.isBlank()) {
        SecurityContextHolder.clearContext();
        response.sendError(
                HttpServletResponse.SC_UNAUTHORIZED,
                "Invalid authentication token"
        );
        return;
    }

    String email;
    try {
        email = tokenService.validateToken(token);
    } catch (JWTVerificationException exception) {
        SecurityContextHolder.clearContext();
        response.sendError(
                HttpServletResponse.SC_UNAUTHORIZED,
                "Invalid authentication token"
        );
        return;
    }

    if (email == null || email.isBlank()) {
        SecurityContextHolder.clearContext();
        response.sendError(
                HttpServletResponse.SC_UNAUTHORIZED,
                "Invalid authentication token"
        );
        return;
    }

    UserDetails user = userRepository.findByEmail(email);
    if (user == null) {
        SecurityContextHolder.clearContext();
        response.sendError(
                HttpServletResponse.SC_UNAUTHORIZED,
                "Invalid authentication token"
        );
        return;
    }

    var authentication = new UsernamePasswordAuthenticationToken(
            user,
            null,
            user.getAuthorities()
    );

    var context = SecurityContextHolder.createEmptyContext();
    context.setAuthentication(authentication);
    SecurityContextHolder.setContext(context);

    filterChain.doFilter(request, response);
}

private String recoverToken(HttpServletRequest request) {
    String authHeader = request.getHeader("Authorization");
    String prefix = "Bearer ";

    if (authHeader == null
            || !authHeader.regionMatches(
                    true, 0, prefix, 0, prefix.length()
            )) {
        return null;
    }

    return authHeader.substring(prefix.length()).trim();
}

JWTVerificationException 使用原 TokenService 中的同一个异常类型。Servlet 相关类型的 import 应沿用项目现有配置。

原来的 replace("Bearer ", "") 没有检查认证方案,也没有限定只移除开头的前缀。上面的代码先检查前缀,再截取 Token。

为什么登录接口配置了 permitAll() 还会报错?

permitAll() 允许请求通过对应的授权检查,自定义过滤器仍会执行。你的 SecurityFilter 被加入了过滤器链,因此登录请求如果携带旧 Token 或无效 Token,也会进入 JWT 处理逻辑。

可以先清除客户端对 /auth/login 和 /auth/register 自动附加的 Authorization 请求头,再重试登录,以确认故障是否由客户端携带的 Token 触发。

如果这两个接口按业务设计完全不使用 JWT,也可以通过 OncePerRequestFilter.shouldNotFilter() 跳过它们。匹配时应同时考虑请求方法和路径,并与安全配置保持一致。

仅删除请求头不能替代服务端修复。过滤器仍需正确处理验证失败和用户不存在的情况,避免其他请求再次触发相同异常。

备注:内容仅供参考。