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

Spring Security认证失败URL首次跳转异常及解决

Spring Security 自定义认证失败跳转首次异常问题及解惑

问题场景

为区分凭证错误和账户未激活两种认证失败场景,配置不同跳转URL,我自定义了AuthenticationProvider和AuthenticationFailureHandler,但遇到首次认证失败跳转异常的问题:

  • 访问主页localhost:8080,因未认证被重定向到localhost:8080/login
  • 输入错误凭证,本该跳转到localhost:8080/login?badCredentials,却停留在localhost:8080/login
  • 输入正确凭证,被重定向到localhost:8080/login?badCredentials&continue,但实际已完成登录(Authentication对象已存入SecurityContext)
  • 后续再输入错误凭证或未激活账户时,跳转恢复正常,仅首次出现问题
  • 手动访问主页可正常进入登录状态

相关代码

AuthenticationProvider实现类

@Component
public class AuthenticationProviderImpl implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    @Autowired
    public AuthenticationProviderImpl(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        try {
            String passwordEnteredByUser = authentication.getCredentials().toString();
            UserDetails userDetails = userDetailsService.loadUserByUsername(authentication.getName());

            if (!passwordEncoder.matches(passwordEnteredByUser, userDetails.getPassword())) {
                throw new BadCredentialsException("Bad credentials");
            }

            if (!userDetails.isEnabled()) {
                throw new AccountNotActivatedException("Account not activated");
            }

            return new UsernamePasswordAuthenticationToken(
                    userDetails.getUsername(),
                    userDetails.getPassword(),
                    userDetails.getAuthorities()
            );
        } catch (UsernameNotFoundException e) {
            throw new BadCredentialsException("Bad credentials");
        }
    }

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

AuthenticationFailureHandler实现类

@Setter
@Component
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {

    private String redirectUrl = "login?error";
    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {

        if (exception instanceof BadCredentialsException) {
            setRedirectUrl("login?badCredentials");
        } else if (exception instanceof AccountNotActivatedException) {
            setRedirectUrl("login?notActivated");
        }

        this.redirectStrategy.sendRedirect(request, response, redirectUrl);
    }
}

SecurityConfig配置类

@Configuration
public class SecurityConfig {

    private final AuthenticationFailureHandler authenticationFailureHandler;
    private final AuthenticationProvider authenticationProvider;

    @Autowired
    public SecurityConfig(AuthenticationProvider authenticationProvider,
                          AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationProvider = authenticationProvider;
        this.authenticationFailureHandler = authenticationFailureHandler;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/register").permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin(form -> form
                        .permitAll()
                        .loginPage("/login")
                        .defaultSuccessUrl("/")
                        .failureHandler(authenticationFailureHandler)

                )
                .logout(logout -> logout
                        .logoutUrl("/logout")
                )
                .authenticationProvider(authenticationProvider)
                .build();
    }

    @Bean
    public static PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

解决方案

问题根源是/login路径(包括带参数的请求)未被放行,在SecurityConfig的authorizeHttpRequests中添加requestMatchers("/login").permitAll()即可解决:

修改后的filterChain方法:

@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
    return httpSecurity
            .authorizeHttpRequests(authorize -> authorize
                    .requestMatchers("/register", "/login").permitAll()
                    .anyRequest().authenticated()
            )
            .formLogin(form -> form
                    .permitAll()
                    .loginPage("/login")
                    .defaultSuccessUrl("/")
                    .failureHandler(authenticationFailureHandler)

            )
            .logout(logout -> logout
                    .logoutUrl("/logout")
            )
            .authenticationProvider(authenticationProvider)
            .build();
}

疑惑解答

为什么未添加/login的permitAll()时,未认证状态下登录页面还能正常显示?

这是因为Spring Security的formLogin配置中,当你指定.loginPage("/login")时,框架会自动放行/login的GET请求(用于显示登录页面),但不会自动放行/login的POST请求(认证提交)以及带参数的/login?xxx请求。你的全局规则是anyRequest().authenticated(),所以当首次认证失败后,AuthenticationFailureHandler跳转到login?badCredentials这个GET请求时,会被Security拦截,因为该请求需要认证,进而被重定向回/login,导致你看起来停留在了登录页面,没有跳转到预期的带参数URL。

添加requestMatchers("/login").permitAll()后,所有/login路径的请求(包括带参数的)都被放行,失败跳转的URL就能正常访问,显示对应的错误标识。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 12:25:16