You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

请求验证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:将PasswordEncoder Bean移至WebSecurityConfig或单独的配置类中,遵循单一职责原则:
    // 在WebSecurityConfig中添加
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
  • 明确AuthenticationManager的使用场景:这个CustomAuthenticationManager应该是用于用户名密码登录接口的认证(比如登录时验证用户名密码生成JWT),而不是JWT验证流程的一部分。JWT验证流程由AuthTokenFilter完成,已经设置了认证上下文,正常情况下不会再触发AuthenticationManager的authenticate方法——出现这个问题的原因是当前SecurityFilterChain配置可能存在冲突,移除手动设置的authenticationManager后可解决。

内容的提问来源于stack exchange,提问作者user9492428

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 16:16:04