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

添加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)时无此问题。

问题原因
  1. 自定义Filter配置位置错误:原代码中使用http.addFilterBefore(filter, CustomAuthenticationFilter.class),相当于把自定义Filter加到自身前面,导致Filter未正确替换默认的UsernamePasswordAuthenticationFilter,认证后的Authentication无法被Spring Security正确处理并绑定到请求上下文。
  2. AuthenticationManager配置冲突:手动重复创建ProviderManager,未与Spring Security自动管理的AuthenticationManager关联,导致认证上下文传播异常。
  3. 认证请求传递方式不规范:在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 23:42:06