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. 验证步骤
- 在
AbstractAuthenticationProcessingFilter.successfulAuthentication方法处加断点,确认认证后的Token被存入SecurityContextHolder.getContext()。 - 访问/home时,检查
SecurityContextHolder.getContext().getAuthentication()是否存在且isAuthenticated()返回true。 - 查看Session中的
SPRING_SECURITY_CONTEXT属性,确认包含自定义Token。
内容的提问来源于stack exchange,提问作者Pingus
相关产品推荐
相关产品推荐

