Spring Security 6.2+下基于Mobile-OTP的OAuth 2.1+PKCE流程实现问题
问题整理
现有环境
Spring Boot 3.2+、Spring Security、Spring Authorization Server
需求目标
- 实现基于手机号+OTP的用户登录认证
- 遵循OAuth 2.1 + PKCE规范
问题详情
- 客户端调用
oauth2/authorize端点后会重定向到自定义登录页(位于/resources/static),用户输入手机号和OTP后,需重定向到请求的redirect_uri并携带授权码code,以便后续调用oauth2/token端点。目前已实现自定义认证过滤器验证OTP并返回认证对象,但无法返回redirect_uri?code='',不清楚在Spring中如何实现该流程。 - 当前实现方式是否正确?若不正确,缺少或错误的点是什么?若正确,是否有更优实践或参考资料?
安全配置代码
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授权码流程的链路:
- 固定跳转破坏授权上下文:
formLogin中配置的defaultSuccessUrl(SUCCESS_ENDPOINT)会强制认证成功后跳转到/dashboard,完全忽略用户从oauth2/authorize发起请求时保存的授权上下文(包括redirect_uri、state等参数),导致无法回到授权服务器生成授权码。 - 自定义过滤器未处理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
相关产品推荐
相关产品推荐

