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

Spring Security认证成功后仍重定向至登录页问题求助

自定义Spring Security认证后重定向失效问题

基于Spring Security实现自定义认证功能,已确认认证Provider向Filter正确返回了已认证Token,且“认证成功”逻辑已触发,但即便在成功处理器中指定重定向至/home页面,系统仍会跳转到/login页面。DEBUG日志显示认证后先重定向到/home,但访问/home时被判定为未认证,又跳回/login。


相关代码

SecurityConfig类

@EnableWebSecurity
@Configuration
public class DefaultSecurityConfig {

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }


    @Bean
    public DIDAuthenticationProvider didAuthenticationProvider(DIDUserDetailsService userDetailsService) {
        return new DIDAuthenticationProvider(userDetailsService);
    }

    @Bean
    public DIDUserDetailsService didUserDetailsService(DIDUser... users) {
        return new DIDUserDetailsService(users);
    }

    @Bean
    public DIDAuthenticationFilter authenticationFilter(AuthenticationManager authManager,
                                                        SavedRequestAwareAuthenticationSuccessHandler authenticationSuccessHandler,
                                                        SimpleUrlAuthenticationFailureHandler authenticationFailureHandler) {

        DIDAuthenticationFilter authenticationFilter = new DIDAuthenticationFilter();

        authenticationFilter.setAuthenticationManager(authManager);
        authenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        authenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);

        return authenticationFilter;
    }

    @Bean
    public SavedRequestAwareAuthenticationSuccessHandler authenticationSuccessHandler() {
        SavedRequestAwareAuthenticationSuccessHandler authenticationSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler();
        authenticationSuccessHandler.setDefaultTargetUrl("/home");
        authenticationSuccessHandler.setAlwaysUseDefaultTargetUrl(true);

        return authenticationSuccessHandler;
    }

    @Bean
    public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() {
        SimpleUrlAuthenticationFailureHandler authenticationFailureHandler = new SimpleUrlAuthenticationFailureHandler();
        authenticationFailureHandler.setDefaultFailureUrl("/login?error");
        authenticationFailureHandler.setUseForward(true);
        return authenticationFailureHandler;
    }

    @Bean
    public DIDUser[] users() {
        DIDUser user = new DIDUser(
                "user",
                "did:test:123",
                "USER");
        return new DIDUser[]{user};
    }

    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http,
                                                          DIDAuthenticationFilter authenticationFilter,
                                                          SavedRequestAwareAuthenticationSuccessHandler authenticationSuccessHandler
    ) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        .anyRequest().authenticated()
                )
                .formLogin(form -> form
                        .loginPage("/login")
                        .permitAll()
                )
                .logout(logout -> logout
                        .logoutSuccessUrl("/login?logout")
                        .deleteCookies("JSESSIONID")
                        .permitAll()
                )
                .anonymous(AbstractHttpConfigurer::disable)
                .csrf(AbstractHttpConfigurer::disable)
                .addFilterAt(authenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

DIDAuthenticationFilter类

public class DIDAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public static final String FORM_DID_KEY = "did";

    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/did-login", "POST");

    public DIDAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    public DIDAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        String did = request.getParameter(FORM_DID_KEY);
        if (!request.getMethod().equals("POST") || did == null || StringUtils.isEmptyOrWhitespace(did)) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        did = did.trim();
        DIDAuthenticationToken authRequest = DIDAuthenticationToken.unauthenticated(did, did);
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

DIDAuthenticationProvider类

public class DIDAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {

    private final DIDUserDetailsService users;

    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    public DIDAuthenticationProvider(DIDUserDetailsService users) {
        this.users = users;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String did = determineDID(authentication);

        DIDUser user = null;
        try {
            user = (DIDUser) retrieveUser(did);
        } catch (AuthenticationException ex) {
            throw new BadCredentialsException(this.messages
                    .getMessage("badCredentials", "Bad credentials"));
        }

        Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");

        Object principalToReturn = user;
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

    private UserDetails retrieveUser(String did) throws AuthenticationException {
        try {
            UserDetails loadedUser = this.users.loadUserByDid(did);

            if (loadedUser == null) {
                throw new UsernameNotFoundException("UserDetailsService returned null");
            }
            return loadedUser;
        } catch (Exception e) {
            throw new UsernameNotFoundException("UserDetailsService returned null");
        }
    }

    private String determineDID(Authentication authentication) {
        return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
    }

    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
                                                         UserDetails user) {
        DIDAuthenticationToken result = DIDAuthenticationToken.authenticated(principal, authentication.getCredentials(), user.getAuthorities());
        result.setDetails(authentication.getDetails());

        return result;
    }

    @Override
    public void afterPropertiesSet() {
        Assert.notNull(this.messages, "A message source must be set");
        Assert.notNull(this.users, "A UserDetailsService must be set");
    }

    @Override
    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(DIDAuthenticationToken.class);
    }
}

DEBUG日志

2023-05-12T12:27:55.311+02:00 DEBUG 9680 --- [nio-9000-exec-4] o.s.security.web.FilterChainProxy        : Securing POST /did-login
2023-05-12T12:27:55.312+02:00 DEBUG 9680 --- [nio-9000-exec-4] o.s.s.web.DefaultRedirectStrategy        : Redirecting to /home
2023-05-12T12:27:55.320+02:00 DEBUG 9680 --- [nio-9000-exec-7] o.s.security.web.FilterChainProxy        : Securing GET /home
2023-05-12T12:27:55.324+02:00 DEBUG 9680 --- [nio-9000-exec-7] o.s.s.w.s.HttpSessionRequestCache        : Saved request http://localhost:9000/home?continue to session
2023-05-12T12:27:55.324+02:00 DEBUG 9680 --- [nio-9000-exec-7] o.s.s.web.DefaultRedirectStrategy        : Redirecting to http://localhost:9000/login
2023-05-12T12:27:55.333+02:00 DEBUG 9680 --- [nio-9000-exec-6] o.s.security.web.FilterChainProxy        : Securing GET /login
2023-05-12T12:27:55.333+02:00 DEBUG 9680 --- [nio-9000-exec-6] o.s.security.web.FilterChainProxy        : Secured GET /login

问题诊断与解决

从日志能看到,认证成功后先重定向到/home,但访问/home时被判定为未认证,说明认证后的Token没有被正确保存到SecurityContext并在后续请求中被识别,核心问题出在自定义Token的实现和Spring Security的认证上下文持久化逻辑。

1. 确保自定义Token继承AbstractAuthenticationToken

Spring Security依赖AbstractAuthenticationToken来识别已认证的Token,检查DIDAuthenticationToken是否正确继承该类并实现必要方法:

public class DIDAuthenticationToken extends AbstractAuthenticationToken {
    private final Object principal;
    private Object credentials;

    public DIDAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        // 已认证Token必须设置为true
        setAuthenticated(!authorities.isEmpty());
    }

    // 静态工厂方法,统一创建未认证/已认证Token
    public static DIDAuthenticationToken unauthenticated(String principal, String credentials) {
        return new DIDAuthenticationToken(principal, credentials, Collections.emptyList());
    }

    public static DIDAuthenticationToken authenticated(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        return new DIDAuthenticationToken(principal, credentials, authorities);
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

2. 手动注册自定义AuthenticationProvider

默认情况下Spring Security会自动注册AuthenticationProvider Bean,但版本兼容问题可能导致注册失败,需手动添加到AuthenticationManager:

@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http,
                                                      DIDAuthenticationFilter authenticationFilter,
                                                      SavedRequestAwareAuthenticationSuccessHandler authenticationSuccessHandler,
                                                      DIDAuthenticationProvider didAuthenticationProvider
) throws Exception {
    // 手动将自定义Provider加入AuthenticationManager
    http.getSharedObject(AuthenticationManagerBuilder.class)
            .authenticationProvider(didAuthenticationProvider);
            
    http
            .authorizeHttpRequests(authorize -> authorize
                    .anyRequest().authenticated()
            )
            .formLogin(form -> form
                    .loginPage("/login")
                    .permitAll()
            )
            .logout(logout -> logout
                    .logoutSuccessUrl("/login?logout")
                    .deleteCookies("JSESSIONID")
                    .permitAll()
            )
            .anonymous(AbstractHttpConfigurer::disable)
            .csrf(AbstractHttpConfigurer::disable)
            .addFilterAt(authenticationFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build();
}

3. 确保Token可序列化(Session认证场景)

如果应用基于Session认证,自定义Token必须实现Serializable接口,否则无法存入Session:

public class DIDAuthenticationToken extends AbstractAuthenticationToken implements Serializable {
    // ... 其他实现
}

4. 验证步骤

  1. 在AbstractAuthenticationProcessingFilter.successfulAuthentication方法处加断点,确认认证后的Token被存入SecurityContextHolder.getContext()。
  2. 访问/home时,检查SecurityContextHolder.getContext().getAuthentication()是否存在且isAuthenticated()返回true。
  3. 查看Session中的SPRING_SECURITY_CONTEXT属性,确认包含自定义Token。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 23:37:01