请求验证Spring Security + JWT的REST API认证实现正确性
Spring Security JWT认证实现验证与问题修复
背景
我基于Spring Security实现JWT认证保护REST API,代码无运行异常,但因参考已废弃的WebSecurityConfigurerAdapter迁移指南拼凑而成,希望验证实现是否符合规范。
核心代码
WebSecurityConfig.java
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class WebSecurityConfig { private final CustomAuthenticationManager customAuthenticationManager; private final AuthTokenFilter authTokenFilter; private final AuthEntryPoint authEntryPoint; @Value("${api.prefix}") private String apiPrefix; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .cors().and().csrf().disable() .exceptionHandling().authenticationEntryPoint(null).and() .authenticationManager(customAuthenticationManager) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(apiPrefix + "/auth/**").permitAll() .antMatchers(apiPrefix + "/test/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling( httpSecurityExceptionHandlingConfigurer -> httpSecurityExceptionHandlingConfigurer .authenticationEntryPoint(authEntryPoint) ); http.addFilterBefore(authTokenFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }
AuthTokenFilter.java
@Slf4j @Component @RequiredArgsConstructor public class AuthTokenFilter extends OncePerRequestFilter { private final JwtUtils jwtUtils; private final UserDAO userDAO; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String jwt = parseJwt(request); if (Objects.isNull(jwt)) { throw new AuthenticationCredentialsNotFoundException("Unable to extract JWT token from authentication header"); } try { if (jwtUtils.validateJwtToken(jwt)) { String username = jwtUtils.getUserNameFromJwtToken(jwt); UserDetails userDetails = userDAO.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken.authenticated(userDetails, null, new ArrayList<>()); // 请检查此行 authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); filterChain.doFilter(request, response); } } catch (AuthenticationException e) { throw e; } catch (Exception e) { log.error("Cannot set user authentication: {}", e.getMessage(), e); throw new CustomRTException("Error while validating jwt token", HttpStatus.UNAUTHORIZED); } } private String parseJwt(HttpServletRequest request) { String headerAuth = request.getHeader("Authorization"); if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { return headerAuth.substring(7); } return null; } }
CustomAuthenticationManager.java
@Slf4j @Component @RequiredArgsConstructor public class CustomAuthenticationManager implements AuthenticationManager { private final UserDAO userDAO; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { UserDetails userDetails = userDAO.loadUserByUsername(authentication.getName()); if (passwordEncoder().matches(authentication.getCredentials().toString(), userDetails.getPassword())) { throw new BadCredentialsException("Wrong Password"); } return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword()); } }
遇到的核心问题
在AuthTokenFilter中验证JWT令牌后,Spring仍会调用CustomAuthenticationManager#authenticate,导致authentication.getCredentials().toString()触发NullPointerException——因为JWT解析后无法获取密码,credentials为null。
我通过以下代码临时解决:
UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken.authenticated(userDetails, null, new ArrayList<>());
(目前不需要权限,所以传空列表)
验证与优化建议
1. WebSecurityConfig 优化
- 合并重复的异常处理配置:当前代码先设置
authenticationEntryPoint(null)又重新配置authEntryPoint,应合并为一次配置,避免逻辑混乱:http .cors().and().csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(apiPrefix + "/auth/**").permitAll() .antMatchers(apiPrefix + "/test/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(authEntryPoint); - 移除手动设置authenticationManager:Spring Security会自动识别容器中的AuthenticationManager Bean,无需手动配置,除非有特殊的多管理器场景。
2. AuthTokenFilter 优化
- 放行无JWT的公开端点:当前代码在无JWT时直接抛出异常,会导致
/auth/**、/test/**等公开端点也被拦截报错,应改为:if (Objects.isNull(jwt)) { filterChain.doFilter(request, response); return; } - 权限列表使用userDetails的默认权限:即使当前不需要权限,也应保持扩展性,使用
userDetails.getAuthorities()代替空列表:UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken.authenticated( userDetails, null, userDetails.getAuthorities() ); - 细化异常处理:JWT无效、过期等场景可以单独抛出对应异常,更便于前端处理。
3. CustomAuthenticationManager 修复与优化
- 修复密码匹配逻辑:当前逻辑完全写反,
passwordEncoder.matches返回true表示密码匹配,应改为不匹配时抛出异常:if (!passwordEncoder().matches(authentication.getCredentials().toString(), userDetails.getPassword())) { throw new BadCredentialsException("Wrong Password"); } - 拆分PasswordEncoder Bean:将
PasswordEncoderBean移至WebSecurityConfig或单独的配置类中,遵循单一职责原则:// 在WebSecurityConfig中添加 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - 明确AuthenticationManager的使用场景:这个CustomAuthenticationManager应该是用于用户名密码登录接口的认证(比如登录时验证用户名密码生成JWT),而不是JWT验证流程的一部分。JWT验证流程由AuthTokenFilter完成,已经设置了认证上下文,正常情况下不会再触发AuthenticationManager的authenticate方法——出现这个问题的原因是当前SecurityFilterChain配置可能存在冲突,移除手动设置的authenticationManager后可解决。
内容的提问来源于stack exchange,提问作者user9492428
相关产品推荐
相关产品推荐

