介绍
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();
}
@Bean
public 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);
}
@Bean
public 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();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
MobilePhoneAuthenticationConverter.java
public class MobilePhoneAuthenticationConverter implements AuthenticationConverter {
@Override
public 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 {
@Autowired
private OAuth2AuthorizationService authorizationService;
@Autowired
private OAuth2TokenGenerator<OAuth2Token> tokenGenerator;
@Override
public 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());
}
@Override
public 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);
}
@Override
public Object getCredentials() {
return this.smsCode;
}
@Override
public Object getPrincipal() {
return this.phoneNumber;
}
public String getClientId() {
return this.clientId;
}
}