Spring Authorization Server实现短信验证码登录的OAuth2扩展方案

发布时间:2026/7/18 12:38:19
Spring Authorization Server实现短信验证码登录的OAuth2扩展方案 1. Spring Authorization Server自定义grant_type实现短信认证登录在OAuth2.1协议中密码模式已被废除但很多老系统仍依赖这种认证方式。Spring Authorization Server提供了扩展机制让我们可以自定义grant_type来实现短信验证码登录。这种方案既兼容了老系统需求又符合现代认证安全规范。1.1 核心实现原理自定义grant_type需要实现三个核心组件AuthenticationToken继承AbstractAuthenticationToken封装认证请求参数AuthenticationConverter将HTTP请求转换为认证TokenAuthenticationProvider执行实际的认证逻辑这种设计遵循了Spring Security的认证流程HTTP请求 → Converter生成Token → Provider执行认证 → 返回AccessToken2. 详细实现步骤2.1 定义安全常量首先需要定义短信认证相关的常量public class SecurityConstants { // 短信验证登录类型 public static final String SMS_LOGIN_TYPE smsCaptcha; // 自定义grant_type public static final String GRANT_TYPE_SMS_CODE urn:ietf:params:oauth:grant-type:sms_code; // 请求参数名 public static final String OAUTH_PARAMETER_NAME_PHONE phone; public static final String OAUTH_PARAMETER_NAME_SMS_CAPTCHA sms_captcha; }2.2 实现SmsCaptchaGrantAuthenticationToken继承AbstractAuthenticationToken封装认证信息public class SmsCaptchaGrantAuthenticationToken extends AbstractAuthenticationToken { private final SetString scopes; private final Authentication clientPrincipal; private final MapString, Object additionalParameters; private final AuthorizationGrantType authorizationGrantType; // 构造方法 public SmsCaptchaGrantAuthenticationToken(AuthorizationGrantType authorizationGrantType, Authentication clientPrincipal, SetString scopes, MapString, Object additionalParameters) { super(Collections.emptyList()); this.scopes scopes; this.clientPrincipal clientPrincipal; this.additionalParameters additionalParameters; this.authorizationGrantType authorizationGrantType; } // 实现抽象方法 Override public Object getCredentials() { return null; } Override public Object getPrincipal() { return clientPrincipal; } // Getter方法 public SetString getScopes() { return this.scopes; } public AuthorizationGrantType getAuthorizationGrantType() { return this.authorizationGrantType; } public MapString, Object getAdditionalParameters() { return this.additionalParameters; } }2.3 实现AuthenticationConverter将HTTP请求转换为认证Tokenpublic class SmsCaptchaGrantAuthenticationConverter implements AuthenticationConverter { Override public Authentication convert(HttpServletRequest request) { // 校验grant_type String grantType request.getParameter(OAuth2ParameterNames.GRANT_TYPE); if (!SecurityConstants.GRANT_TYPE_SMS_CODE.equals(grantType)) { return null; } // 获取请求参数 MultiValueMapString, String parameters SecurityUtils.getParameters(request); // 校验scope String scope parameters.getFirst(OAuth2ParameterNames.SCOPE); SetString requestedScopes null; if (StringUtils.hasText(scope)) { requestedScopes new HashSet(Arrays.asList(scope.split( ))); } // 校验手机号和验证码参数 String phone parameters.getFirst(SecurityConstants.OAUTH_PARAMETER_NAME_PHONE); String smsCaptcha parameters.getFirst(SecurityConstants.OAUTH_PARAMETER_NAME_SMS_CAPTCHA); if (!StringUtils.hasText(phone) || !StringUtils.hasText(smsCaptcha)) { throwError(OAuth2ErrorCodes.INVALID_REQUEST, Missing required parameters, null); } // 构建认证Token MapString, Object additionalParameters new HashMap(); additionalParameters.put(SecurityConstants.OAUTH_PARAMETER_NAME_PHONE, phone); additionalParameters.put(SecurityConstants.OAUTH_PARAMETER_NAME_SMS_CAPTCHA, smsCaptcha); return new SmsCaptchaGrantAuthenticationToken( new AuthorizationGrantType(SecurityConstants.GRANT_TYPE_SMS_CODE), SecurityContextHolder.getContext().getAuthentication(), requestedScopes, additionalParameters ); } }2.4 实现AuthenticationProvider执行实际的认证逻辑Slf4j public class SmsCaptchaGrantAuthenticationProvider implements AuthenticationProvider { private OAuth2TokenGenerator? tokenGenerator; private AuthenticationManager authenticationManager; private OAuth2AuthorizationService authorizationService; Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCaptchaGrantAuthenticationToken authToken (SmsCaptchaGrantAuthenticationToken) authentication; // 校验客户端 OAuth2ClientAuthenticationToken clientPrincipal SecurityUtils.getAuthenticatedClientElseThrowInvalidClient(authToken); RegisteredClient registeredClient clientPrincipal.getRegisteredClient(); // 校验scope SetString authorizedScopes getAuthorizedScopes(registeredClient, authToken.getScopes()); // 执行用户认证 Authentication userAuth authenticateUser(authToken); // 构建Token上下文 DefaultOAuth2TokenContext.Builder tokenContextBuilder DefaultOAuth2TokenContext.builder() .registeredClient(registeredClient) .principal(userAuth) .authorizedScopes(authorizedScopes) .authorizationGrantType(authToken.getAuthorizationGrantType()); // 生成AccessToken OAuth2TokenContext tokenContext tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build(); OAuth2Token generatedAccessToken this.tokenGenerator.generate(tokenContext); // 生成RefreshToken(如果支持) OAuth2RefreshToken refreshToken generateRefreshTokenIfSupported(registeredClient, clientPrincipal, tokenContextBuilder); // 生成ID Token(如果请求了openid scope) OidcIdToken idToken generateIdTokenIfRequested(authorizedScopes, tokenContextBuilder); // 保存授权信息 OAuth2Authorization authorization buildAuthorization(registeredClient, userAuth, authorizedScopes, authToken, generatedAccessToken, refreshToken, idToken); this.authorizationService.save(authorization); // 返回认证结果 return new OAuth2AccessTokenAuthenticationToken( registeredClient, clientPrincipal, (OAuth2AccessToken)generatedAccessToken, refreshToken, idToken ! null ? Map.of(OidcParameterNames.ID_TOKEN, idToken.getTokenValue()) : Map.of() ); } // 其他辅助方法... }3. 配置授权服务器最后需要在授权服务器配置中添加我们的自定义grant_typeBean public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); // 创建自定义Converter和Provider SmsCaptchaGrantAuthenticationConverter converter new SmsCaptchaGrantAuthenticationConverter(); SmsCaptchaGrantAuthenticationProvider provider new SmsCaptchaGrantAuthenticationProvider(); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .tokenEndpoint(tokenEndpoint - tokenEndpoint .accessTokenRequestConverter(converter) .authenticationProvider(provider)); // 获取并设置Provider所需的依赖 DefaultSecurityFilterChain build http.build(); provider.setTokenGenerator(http.getSharedObject(OAuth2TokenGenerator.class)); provider.setAuthorizationService(http.getSharedObject(OAuth2AuthorizationService.class)); provider.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); return build; }4. 短信验证码认证实现在短信验证码认证Provider中我们需要验证手机号和验证码Component public class SmsCaptchaLoginAuthenticationProvider extends DaoAuthenticationProvider { Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { // 获取当前请求 HttpServletRequest request ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); // 获取登录类型 String loginType request.getParameter(loginType); String grantType request.getParameter(grant_type); // 如果是短信登录或自定义grant_type if (Objects.equals(loginType, SecurityConstants.SMS_LOGIN_TYPE) || Objects.equals(grantType, SecurityConstants.GRANT_TYPE_SMS_CODE)) { // 验证短信验证码 String sessionCaptcha (String) request.getSession(false) .getAttribute((String) authentication.getPrincipal()); if (!Objects.equals(sessionCaptcha, authentication.getCredentials())) { throw new BadCredentialsException(短信验证码错误); } } else { // 其他情况走默认密码验证 super.additionalAuthenticationChecks(userDetails, authentication); } } }5. 测试验证可以使用Postman测试自定义grant_type首先获取短信验证码GET /getSmsCaptcha?phone13800138000然后使用自定义grant_type获取tokenPOST /oauth2/token Content-Type: application/x-www-form-urlencoded Authorization: Basic base64(clientId:clientSecret) grant_typeurn:ietf:params:oauth:grant-type:sms_code phone13800138000 sms_captcha123456 scopemessage.read6. 注意事项与最佳实践验证码存储实际项目中验证码应该存储在Redis等缓存中并设置有效期而不是Session防刷机制需要对短信接口做限流防止恶意刷短信安全加固验证码建议使用6位以上随机数同一验证码最多验证3次失败后失效每日单个手机号发送上限建议设置为10次性能考虑短信服务建议异步化处理验证码校验接口需要做缓存优化日志记录记录短信发送和验证日志对异常登录尝试进行监控这种自定义grant_type方案既保持了OAuth2的标准流程又满足了业务特定的认证需求是传统密码模式的良好替代方案。