介绍
Spring Security OAuth2 默认实现的四种授权模式在实际的应用场景中往往满足不了预期。 需要扩展如下需求:
- 手机号+短信验证码登陆
- 微信授权登录
本次主要通过继承Spring Security OAuth2 抽象类和接口,来实现对oauth2/token接口的手机号+短信的认证授权。
代码
TechStack/springoatuh2
开发环境
- JDK 17
- Spring Boot 3
核心概念和流程
- SecurityFilterChain: 表示Spring Security的过滤器链。实现安全配置和认证扩展配置
- RegisteredClientRepository: 表示自定义的授权客户端信息,需要进行配置。这个客户端信息是oauth2/token中需要进行认证的信息。
- AbstractAuthenticationToken: 表示用户认证信息。 需要对其进行扩展
- AuthenticationProvider: 验证登录信息,实现token的生成。需要对其进行扩展
- AuthenticationConverter: 实现对AbstractAuthenticationToken自定义扩展类的转换。
主要流程就是,实现上述AbstractAuthenticationToken、AuthenticationProvider、AuthenticationConverter三个抽象类和接口的扩展。并通过实现AuthenticationSuccessHandler扩展类,用来返回token给http response中。
AuthorizationServerConfig.java
Bean@Order(Ordered.HIGHEST_PRECEDENCE)public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).tokenEndpoint(tokenEndpoint -> tokenEndpoint// 自定义授权模式转换器.accessTokenRequestConverter(new MobilePhoneAuthenticationConverter()).accessTokenRequestConverter(new UsernamePasswordGrantAuthenticationConverter())// 自定义授权响应.accessTokenResponseHandler(new CustomizerAuthenticationSuccessHandler()).errorResponseHandler(new CustomizerAuthenticationFailureHandler())).oidc(Customizer.withDefaults());http.exceptionHandling((exceptions) -> exceptions.authenticationEntryPoint((request, response, authException) -> {response.setStatus(HttpStatus.UNAUTHORIZED.value());response.setContentType(MediaType.APPLICATION_JSON_VALUE);OAuth2Error error = new OAuth2Error("unauthorized",authException.getMessage(),"https://tools.ietf.org/html/rfc6750#section-3.1");new ObjectMapper().writeValue(response.getOutputStream(), error);}));// 添加自定义的认证提供者http.authenticationProvider(mobilePhoneAuthenticationProvider);return http.build();}@Beanpublic RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()).clientId("mobile-client").clientSecret(passwordEncoder.encode("secret")).clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).authorizationGrantType(new AuthorizationGrantType("mobile_phone")) // 自定义授权类型.redirectUri("http://127.0.0.1:8080/login/oauth2/code/mobile-client").scope("message.read").scope("message.write").tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.REFERENCE) // 设置访问令牌格式为 REFERENCE.build()).build();return new InMemoryRegisteredClientRepository(registeredClient);}@Beanpublic AuthorizationServerSettings authorizationServerSettings() {return AuthorizationServerSettings.builder().build();}
SecurityConfig.java
@Bean@Order(2)public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorize) -> authorize.requestMatchers("/send-sms", "/oauth2/token").permitAll().anyRequest().authenticated()).csrf((csrf) -> csrf.ignoringRequestMatchers("/send-sms", "/oauth2/token"));return http.build();}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
MobilePhoneAuthenticationConverter.java
public class MobilePhoneAuthenticationConverter implements AuthenticationConverter {@Overridepublic Authentication convert(HttpServletRequest request) {String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);if (!"mobile_phone".equals(grantType)) {return null;}String phoneNumber = request.getParameter("phone_number");String smsCode = request.getParameter("sms_code");String clientId = request.getParameter(OAuth2ParameterNames.CLIENT_ID);if (phoneNumber == null || smsCode == null || clientId == null) {throw new OAuth2AuthenticationException(new OAuth2Error("invalid_request"));}return new MobilePhoneAuthenticationToken(phoneNumber, smsCode, clientId);}
}
MobilePhoneAuthenticationProvider.java
@Component
public class MobilePhoneAuthenticationProvider implements AuthenticationProvider {@Autowiredprivate OAuth2AuthorizationService authorizationService;@Autowiredprivate OAuth2TokenGenerator<OAuth2Token> tokenGenerator;@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {MobilePhoneAuthenticationToken mobilePhoneAuthentication = (MobilePhoneAuthenticationToken) authentication;// 验证手机号和验证码的逻辑...String phoneNumber = (String) mobilePhoneAuthentication.getPrincipal();String smsCode = (String) mobilePhoneAuthentication.getCredentials();// 这里应该添加实际的验证逻辑if (!"123456".equals(smsCode)) { // 示例验证,实际应该查询数据库或缓存throw new BadCredentialsException("Invalid SMS code");}OAuth2ClientAuthenticationToken clientPrincipal =getAuthenticatedClientElseThrowInvalidClient();RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder().registeredClient(registeredClient).principal(mobilePhoneAuthentication).authorizationServerContext(AuthorizationServerContextHolder.getContext()).authorizedScopes(registeredClient.getScopes()).tokenType(OAuth2TokenType.ACCESS_TOKEN).authorizationGrantType(new AuthorizationGrantType("mobile_phone")).build();OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);if (!(generatedAccessToken instanceof OAuth2AccessToken)) {throw new OAuth2AuthenticationException(new OAuth2Error("server_error", "The token generator failed to generate the access token.", null));}OAuth2AccessToken accessToken = (OAuth2AccessToken) generatedAccessToken;OAuth2RefreshToken refreshToken = null;if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {tokenContext = DefaultOAuth2TokenContext.builder().registeredClient(registeredClient).principal(mobilePhoneAuthentication).authorizationServerContext(AuthorizationServerContextHolder.getContext()).authorizedScopes(registeredClient.getScopes()).tokenType(OAuth2TokenType.REFRESH_TOKEN).authorizationGrantType(new AuthorizationGrantType("mobile_phone")).build();OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {throw new OAuth2AuthenticationException(new OAuth2Error("server_error", "The token generator failed to generate the refresh token.", null));}refreshToken = (OAuth2RefreshToken) generatedRefreshToken;}OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient).principalName(phoneNumber).authorizationGrantType(new AuthorizationGrantType("mobile_phone")).token(accessToken).refreshToken(refreshToken).build();this.authorizationService.save(authorization);return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, Collections.emptyMap());}@Overridepublic boolean supports(Class<?> authentication) {return MobilePhoneAuthenticationToken.class.isAssignableFrom(authentication);}private OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient() {// 这里需要实现获取当前认证的客户端逻辑// 例如,从 SecurityContextHolder 中获取Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication instanceof OAuth2ClientAuthenticationToken) {return (OAuth2ClientAuthenticationToken) authentication;}throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);}
MobilePhoneAuthenticationToken.java
public class MobilePhoneAuthenticationToken extends AbstractAuthenticationToken {private final String phoneNumber;private final String smsCode;private final String clientId;public MobilePhoneAuthenticationToken(String phoneNumber, String smsCode, String clientId) {super(null);this.phoneNumber = phoneNumber;this.smsCode = smsCode;this.clientId = clientId;setAuthenticated(false);}public MobilePhoneAuthenticationToken(String phoneNumber, String smsCode, String clientId, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.phoneNumber = phoneNumber;this.smsCode = smsCode;this.clientId = clientId;super.setAuthenticated(true);}@Overridepublic Object getCredentials() {return this.smsCode;}@Overridepublic Object getPrincipal() {return this.phoneNumber;}public String getClientId() {return this.clientId;}
}