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、数据库记录和实际请求头,无法仅凭这些代码确定属于哪条路径。
按顺序检查验证结果和查询条件
- 查看失败请求是否携带
Authorization请求头,以及是否符合Bearer <token>格式。 - 在
validateToken()的异常分支设置断点,确认是否发生JWTVerificationException,并查看具体异常类型。 - 检查验证返回的
login是否为空,以及是否与目标用户的邮箱一致。 - 确认
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() 跳过它们。匹配时应同时考虑请求方法和路径,并与安全配置保持一致。
仅删除请求头不能替代服务端修复。过滤器仍需正确处理验证失败和用户不存在的情况,避免其他请求再次触发相同异常。
备注:内容仅供参考。
