添加CustomFilter后Controller中@AuthenticationPrincipal传值为null
在登录页面新增TenantID字段,结合用户名、密码完成用户认证,自定义了CustomAuthenticationFilter、CustomAuthenticationProvider、MethodSecurityConfig、SuccessHandler并配置到SecurityConfig中。登录成功后,SuccessHandler可通过SecurityContextHolder.getContext().getAuthentication().getPrincipal()获取用户数据,但Controller中通过@AuthenticationPrincipal注入的AppUserDetails为null。当前使用SpringBoot3.0.4、SpringSecurity6.0.2,此前使用SpringSecurity5.2.2(基于WebSecurityConfigurerAdapter)时无此问题。
- 自定义Filter配置位置错误:原代码中使用
http.addFilterBefore(filter, CustomAuthenticationFilter.class),相当于把自定义Filter加到自身前面,导致Filter未正确替换默认的UsernamePasswordAuthenticationFilter,认证后的Authentication无法被Spring Security正确处理并绑定到请求上下文。 - AuthenticationManager配置冲突:手动重复创建
ProviderManager,未与Spring Security自动管理的AuthenticationManager关联,导致认证上下文传播异常。 - 认证请求传递方式不规范:在
CustomAuthenticationFilter中直接将未认证的AppUserDetails作为UsernamePasswordAuthenticationToken的principal传递,不符合Spring Security的认证流程规范,后续类型转换时出现异常。
1. 修正自定义Filter的配置位置
替换默认的UsernamePasswordAuthenticationFilter,确保认证流程被正确接管:
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()); http.headers(head -> head.frameOptions().disable()); http .authorizeHttpRequests(authz -> authz .requestMatchers("/login").permitAll() .anyRequest().authenticated()); // 配置表单登录基础参数 http.formLogin(form -> form .loginPage("/login") .successHandler(successHandler) .failureHandler(new SimpleUrlAuthenticationFailureHandler("/login?error"))); // 替换默认的UsernamePasswordAuthenticationFilter CustomAuthenticationFilter filter = new CustomAuthenticationFilter(); filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST")); filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); filter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/login?error")); filter.setAuthenticationSuccessHandler(successHandler); http.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class); // 绑定自定义AuthenticationProvider http.authenticationProvider(authenticationProvider); // 退出配置 http .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .logoutSuccessUrl("/login"); return http.build(); } // 移除手动创建AuthenticationManager的方法,由Spring Security自动管理
2. 调整CustomAuthenticationFilter的认证请求传递
将tenantId存入认证请求的details中,而非直接传递未认证的AppUserDetails:
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String tenantId = request.getParameter("tenantId"); String userId = request.getParameter("userId"); String password = obtainPassword(request); // 创建标准认证请求,principal传入userId,tenantId存入details UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userId, password); // 扩展WebAuthenticationDetails存储tenantId authRequest.setDetails(new WebAuthenticationDetails(request) { private final String tenantIdValue = tenantId; @Override public Map<String, Object> getDetails() { Map<String, Object> details = new HashMap<>(); details.put("tenantId", tenantIdValue); return details; } }); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }
3. 修正CustomAuthenticationProvider的用户查询逻辑
从认证请求的details中获取tenantId,再结合userId查询用户:
@Component("CustomAuthenticationProvider") @Slf4j public class CustomAuthenticationProvider implements AuthenticationProvider { @Autowired UserDetailsServiceImpl service; @Autowired MessageSource messageSource; private static final String BAD_CREDENTIALS = "AbstractUserDetailsAuthenticationProvider.badCredentials"; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String userId = (String) authentication.getPrincipal(); String password = (String) authentication.getCredentials(); // 从details中获取tenantId WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails(); String tenantId = (String) details.getDetails().get("tenantId"); // 从数据库查询用户 AppUserDetails user = (AppUserDetails) service.loadUserByUsernameAndTenantId(userId, tenantId); // 密码校验 checkPassword(password, user.getPassword()); // 用户状态校验 UserDetailsChecker checker = new AccountStatusUserDetailsChecker(); checker.check(user); // 返回认证成功的token,principal为已认证的AppUserDetails return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } private void checkPassword(String rawPassword, String encodedPassword) { PasswordEncoder encoder = new BCryptPasswordEncoder(); if (!encoder.matches(rawPassword, encodedPassword)) { String message = messageSource.getMessage(BAD_CREDENTIALS, null, Locale.getDefault()); throw new BadCredentialsException(message); } } }
4. 确保AppUserDetails正确实现UserDetails接口
保证AppUserDetails完整实现UserDetails的所有方法(如getUsername()、getAuthorities()等),确保Spring Security能识别其为合法的用户详情对象。
内容的提问来源于stack exchange,提问作者kassyiKuni

