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

Spring Security 6.2+下基于Mobile-OTP的OAuth 2.1+PKCE流程实现问题

问题整理

现有环境

Spring Boot 3.2+、Spring Security、Spring Authorization Server

需求目标

  • 实现基于手机号+OTP的用户登录认证
  • 遵循OAuth 2.1 + PKCE规范

问题详情

  1. 客户端调用oauth2/authorize端点后会重定向到自定义登录页(位于/resources/static),用户输入手机号和OTP后,需重定向到请求的redirect_uri并携带授权码code,以便后续调用oauth2/token端点。目前已实现自定义认证过滤器验证OTP并返回认证对象,但无法返回redirect_uri?code='',不清楚在Spring中如何实现该流程。
  2. 当前实现方式是否正确?若不正确,缺少或错误的点是什么?若正确,是否有更优实践或参考资料?

安全配置代码

public class SecurityConfig {

    @Autowired
    private OTPAuthenticationProvider authProvider;

    private static final String LOGIN_PAGE_ENDPOINT = "/index.html";
    private static final String LOGIN_ENDPOINT = "/login";
    private static final String LOGIN_ERROR_ENDPOINT = "/login?error";
    private static final String LOGOUT_ENDPOINT = "/logout";
    private static final String SUCCESS_ENDPOINT = "/dashboard";

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());

        http
                .exceptionHandling(
                        c -> c.defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint(LOGIN_ENDPOINT),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                );

        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain appFilterChain(HttpSecurity http) throws Exception {
        http
                .getSharedObject(AuthenticationManagerBuilder.class)
                .authenticationProvider(authProvider);

        http
                .cors().disable()
                .csrf().disable()
                .addFilterBefore(otpAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/login", "/oauth2/**","/assets/**",
                                "/api/otp/generate", "/api/otp/verify", "/api/users/register").permitAll()
                        .anyRequest().authenticated())
                .formLogin(form -> form
                        .loginPage(LOGIN_PAGE_ENDPOINT)
                        .permitAll()
                        .defaultSuccessUrl(SUCCESS_ENDPOINT)
                        .failureHandler(new SimpleUrlAuthenticationFailureHandler(LOGIN_ERROR_ENDPOINT))
                )
                .logout(
                        form -> form.invalidateHttpSession(true).clearAuthentication(true)
                                .logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_ENDPOINT))
                                .logoutSuccessUrl(LOGIN_ENDPOINT + "?logout")
                                .permitAll()
                );

        return http.build();
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }

    @Bean
    public AuthenticationManager authenticationManager() {
        return new ProviderManager(Collections.singletonList(authProvider));
    }

    @Bean
    public OTPAuthenticationFilter otpAuthenticationFilter() {
        return new OTPAuthenticationFilter(LOGIN_ENDPOINT, authenticationManager());
    }

}

编辑补充(5月17日)

  • 经调试发现问题可能出在AuthenticationProvider的实现方式上
  • /login POST请求后未回到OAuth2AuthorizationCodeRequestAuthenticationProvider,导致无法重定向到指定的redirect_uri

问题分析与解决方案

核心问题定位

当前实现存在两个关键问题,直接打断了OAuth2授权码流程的链路:

  1. 固定跳转破坏授权上下文:formLogin中配置的defaultSuccessUrl(SUCCESS_ENDPOINT)会强制认证成功后跳转到/dashboard,完全忽略用户从oauth2/authorize发起请求时保存的授权上下文(包括redirect_uri、state等参数),导致无法回到授权服务器生成授权码。
  2. 自定义过滤器未处理SavedRequest:OTPAuthenticationFilter可能未使用Spring Security的SavedRequestAwareAuthenticationSuccessHandler,无法自动恢复之前的授权请求,进而无法触发OAuth2AuthorizationCodeRequestAuthenticationProvider的逻辑。

另外,若AuthenticationProvider返回的Authentication对象未正确标记为已认证(authenticated = true),或缺少必要权限信息,也会导致Spring Security不认为认证完成,无法推进到授权流程。

修正步骤

1. 移除固定成功跳转,恢复授权上下文

修改appFilterChain中的formLogin配置,去掉defaultSuccessUrl,改用SavedRequestAwareAuthenticationSuccessHandler,让框架自动处理授权请求的恢复:

.formLogin(form -> form
        .loginPage(LOGIN_PAGE_ENDPOINT)
        .permitAll()
        .successHandler(new SavedRequestAwareAuthenticationSuccessHandler()) // 自动恢复之前的SavedRequest
        .failureHandler(new SimpleUrlAuthenticationFailureHandler(LOGIN_ERROR_ENDPOINT))
)

2. 调整自定义OTP过滤器的成功处理器

如果OTPAuthenticationFilter是自定义实现,确保认证成功逻辑使用SavedRequestAwareAuthenticationSuccessHandler,而非硬编码跳转:

public class OTPAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public OTPAuthenticationFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
        super(defaultFilterProcessesUrl, authenticationManager);
        // 设置成功处理器
        setAuthenticationSuccessHandler(new SavedRequestAwareAuthenticationSuccessHandler());
        // 设置失败处理器
        setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(LOGIN_ERROR_ENDPOINT));
    }

    // 其他认证逻辑...
}

3. 确保AuthenticationProvider返回有效认证对象

检查OTPAuthenticationProvider的authenticate方法,返回的Authentication对象必须是已认证状态,示例如下:

public class OTPAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        OTPAuthenticationToken authToken = (OTPAuthenticationToken) authentication;
        String phone = authToken.getPhone();
        String otp = authToken.getOtp();

        // 验证OTP逻辑...
        boolean valid = validateOTP(phone, otp);
        if (!valid) {
            throw new BadCredentialsException("无效的OTP");
        }

        // 构建已认证的Authentication对象,需包含用户权限
        UserDetails userDetails = loadUserByPhone(phone);
        return new UsernamePasswordAuthenticationToken(
                userDetails,
                null,
                userDetails.getAuthorities()
        );
    }

    // 其他方法...
}

4. 验证客户端配置

确保OAuth2客户端已正确配置:

  • 授权类型包含authorization_code
  • 已配置合法的redirect_uri
  • 开启PKCE(Spring Authorization Server默认支持,客户端需正确生成code_challenge)

更优实践

  • 复用框架内置机制:尽量使用Spring Security提供的SavedRequest、SuccessHandler等组件,避免硬编码跳转逻辑,减少对授权流程的干扰。
  • 分离认证与授权逻辑:自定义认证只负责验证手机号+OTP的合法性,授权流程完全交给Spring Authorization Server处理,不要手动干预授权码的生成和跳转。
  • 调试时查看Session:可通过调试工具查看Session中的SPRING_SECURITY_SAVED_REQUEST属性,确认授权请求的上下文是否被正确保存。

内容的提问来源于stack exchange,提问作者Mr. GT

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 10:05:59