Spring Security 6.1.3中AuthenticationManager为空问题求助
Spring Security 6.1.3 + Spring Boot 3.1.3:AuthenticationManager为空导致NPE问题
问题描述
使用Spring Security 6.1.3和Spring Boot 3.1.3,尝试通过Basic Auth连接受保护服务并获取JWT令牌,但无论如何创建AuthenticationManager,它始终为空,抛出NullPointerException。
请求信息
POST http://localhost:8081/login 请求头中携带Basic Auth JSON请求体: { "username": "shopping_list_username", "password": "shopping_list_password" }
相关代码
ApplicationSecurityConfig.java
@Configuration @EnableWebSecurity @EnableMethodSecurity @EnableGlobalAuthentication public class ApplicationSecurityConfig { @Value("${application.access.username}") private String username; @Value("${application.access.password}") private String password; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity .csrf(AbstractHttpConfigurer::disable) .addFilter(new JwtRestAPIAuthenticationFilter(httpSecurity.getSharedObject(AuthenticationManager.class))) .authorizeHttpRequests((authorize) -> authorize .requestMatchers(HttpMethod.GET, "/**").hasAnyAuthority("ADMIN") .requestMatchers(HttpMethod.GET, "/login").hasAnyAuthority("ADMIN") .anyRequest().authenticated() ) .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .httpBasic(withDefaults()) .formLogin(withDefaults()); return httpSecurity.build(); } @Bean public InMemoryUserDetailsManager userDetailsService() { UserDetails user = User .withUsername(username) .password(password) .authorities("ADMIN") .build(); return new InMemoryUserDetailsManager(user); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }
JwtRestAPIAuthenticationFilter.java
public class JwtRestAPIAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private final AuthenticationManager authenticationManager; public JwtRestAPIAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { UsernameAndPasswordRequest authenticationRequest = new ObjectMapper().readValue(request.getInputStream(), UsernameAndPasswordRequest.class); Authentication authentication = new UsernamePasswordAuthenticationToken( authenticationRequest.getUsername(), authenticationRequest.getPassword() ); return authenticationManager.authenticate(authentication); } catch(IOException e){ throw new RuntimeException(e); } } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { String token = Jwts.builder() .setSubject(authResult.getName()) .setIssuedAt(new Date()) .setExpiration(java.sql.Date.valueOf(LocalDate.now().plusWeeks(2))) .signWith(Keys.hmacShaKeyFor(Utils.SECURE_KEY.getBytes())) .compact(); response.addHeader("Authorization", "Bearer " + token); } }
报错信息
java.lang.NullPointerException: Cannot invoke "org.springframework.security.authentication.AuthenticationManager.authenticate(org.springframework.security.core.Authentication)" because the return value of "org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.getAuthenticationManager()" is null at org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.attemptAuthentication(UsernamePasswordAuthenticationFilter.java:85)
已尝试的解决方法
- 通过Bean创建AuthenticationManager:
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) - 从HttpSecurity对象中获取:
http.getSharedObject(AuthenticationManager.class) - 参考Spring官方迁移文章调整代码,但仍无法解决问题。
解决方案
1. 修复自定义Filter的AuthenticationManager注入(直接解决NPE)
你的自定义Filter继承了UsernamePasswordAuthenticationFilter,父类自身维护了AuthenticationManager字段,但你在构造函数中仅给自己定义的字段赋值,没有给父类的字段赋值。父类的attemptAuthentication方法调用的是自身的getAuthenticationManager(),所以返回null导致报错。
修改Filter的构造函数,调用父类的构造函数传递AuthenticationManager:
public class JwtRestAPIAuthenticationFilter extends UsernamePasswordAuthenticationFilter { public JwtRestAPIAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); // 调用父类构造函数,自动赋值父类的authenticationManager字段 } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { UsernameAndPasswordRequest authenticationRequest = new ObjectMapper().readValue(request.getInputStream(), UsernameAndPasswordRequest.class); Authentication authentication = new UsernamePasswordAuthenticationToken( authenticationRequest.getUsername(), authenticationRequest.getPassword() ); // 直接使用父类已初始化的AuthenticationManager return getAuthenticationManager().authenticate(authentication); } catch(IOException e){ throw new RuntimeException(e); } } // 其他方法不变 }
2. 正确注入AuthenticationManager到SecurityFilterChain
构建SecurityFilterChain时,不要从httpSecurity.getSharedObject()获取AuthenticationManager(此时还未初始化),直接注入你定义的AuthenticationManager Bean:
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, AuthenticationManager authenticationManager) throws Exception { httpSecurity .csrf(AbstractHttpConfigurer::disable) // 注入AuthenticationManager并设置Filter处理路径为/login .addFilter(new JwtRestAPIAuthenticationFilter(authenticationManager).setFilterProcessesUrl("/login")) .authorizeHttpRequests((authorize) -> authorize // 允许POST /login匿名访问,否则无法进入登录流程 .requestMatchers(HttpMethod.POST, "/login").permitAll() .requestMatchers(HttpMethod.GET, "/**").hasAnyAuthority("ADMIN") .anyRequest().authenticated() ) .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 移除默认的httpBasic和formLogin,避免与自定义Filter冲突 .httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable); return httpSecurity.build(); }
3. 添加PasswordEncoder并加密密码
Spring Security 6默认要求密码必须经过编码,否则会导致认证失败。添加PasswordEncoder Bean并加密用户密码:
// 在ApplicationSecurityConfig中添加密码编码器Bean @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // 修改userDetailsService方法,注入PasswordEncoder并加密明文密码 @Bean public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) { UserDetails user = User .withUsername(username) .password(passwordEncoder.encode(password)) .authorities("ADMIN") .build(); return new InMemoryUserDetailsManager(user); }
4. 移除废弃注解
@EnableGlobalAuthentication在Spring Security 6中已废弃,建议从ApplicationSecurityConfig中移除该注解,避免潜在问题。
验证修改后的流程
- 发送POST /login请求,携带JSON格式的用户名密码(无需Basic Auth,自定义Filter会直接解析请求体)
- 自定义Filter完成认证后,返回JWT令牌在Authorization响应头中
- 后续请求携带Bearer令牌即可访问受保护的GET接口
内容的提问来源于stack exchange,提问作者CJJ
相关产品推荐
相关产品推荐

