架构版本
Spring Boot 3.1
Spring Authorization Server 1.1.1
spring-cloud 2022.0.3
spring-cloud-alibaba 2022.0.0.0
完整代码👉watermelon-cloud
Spring Authorization Server 授权服务常见的授权模式
授权码模式-->authorization_code
密码模式-->password
客户端模式-->client_credentialsSpring Authorization Server 内置的 授权模式 都定义再
AuthorizationGrantType里面了public final class AuthorizationGrantType implements Serializable { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; public static final AuthorizationGrantType AUTHORIZATION_CODE = new AuthorizationGrantType("authorization_code"); public static final AuthorizationGrantType REFRESH_TOKEN = new AuthorizationGrantType("refresh_token"); public static final AuthorizationGrantType CLIENT_CREDENTIALS = new AuthorizationGrantType("client_credentials"); @Deprecated public static final AuthorizationGrantType PASSWORD = new AuthorizationGrantType("password"); public static final AuthorizationGrantType JWT_BEARER = new AuthorizationGrantType( "urn:ietf:params:oauth:grant-type:jwt-bearer"); public static final AuthorizationGrantType DEVICE_CODE = new AuthorizationGrantType( "urn:ietf:params:oauth:grant-type:device_code"); private final String value; }
如果有一些特定的场景,内置的授权模式不支持,那我们如何去扩展呢?
我也不知道怎么去扩展,那我们就看看 授权码模式 是怎么玩的,然后参考它的去做扩展。
还是先从入口看,前面我们也讲到了,/oauth2/token 对于的
OAuth2TokenEndpointFilter核心代码也就是之前提到的2行代码Authentication authorizationGrantAuthentication = this.authenticationConverter.convert(request); OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =(OAuth2AccessTokenAuthenticationToken) >this.authenticationManager.authenticate(authorizationGrantAuthentication);授权码模式
OAuth2AuthorizationCodeAuthenticationConverter、OAuth2AuthorizationCodeAuthenticationToken、OAuth2AuthorizationCodeAuthenticationProvider是怎么玩的,我们就怎么玩。
那我现在理解搞定这 OAuth2AuthorizationCodeAuthenticationConverter、
OAuth2AuthorizationCodeAuthenticationToken、OAuth2AuthorizationCodeAuthenticationProvider 3个就行了。
手机号+短信验证码模式扩展
参考授权码模式,就是需要
AuthenticationConverter、AuthenticationToken、AuthenticationProvider 对于的实现
SmsAuthenticationToken
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
/**
* 认证类型
*/
private AuthorizationGrantType authorizationGrantType;
/**
* 用户名
*/
private Authentication clientPrincipal;
/**
* 手机号
*/
private String phone;
/**
* 短信验证码
*/
private String code;
/**
* scopes
*/
private Set<String> scopes;
/**
* 扩展的参数
*/
private Map<String, Object> additionalParameters;
public SmsAuthenticationToken(
AuthorizationGrantType authorizationGrantType,
Authentication clientPrincipal,
Set<String> scopes,
String phone,
String code,
Map<String, Object> additionalParameters) {
super(Collections.emptyList());
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
this.scopes = Collections.unmodifiableSet(
scopes != null ?
new HashSet<>(scopes) :
Collections.emptySet());
this.phone = phone;
this.code = code;
this.clientPrincipal = clientPrincipal;
this.additionalParameters = Collections.unmodifiableMap(
additionalParameters != null ?
new HashMap<>(additionalParameters) :
Collections.emptyMap());
this.authorizationGrantType = authorizationGrantType;
}
/**
* 扩展模式一般不需要密码
*/
@Override
public Object getCredentials() {
return null;
}
/**
* 获取用户名
*/
@Override
public Object getPrincipal() {
return this.clientPrincipal;
}
public String getPhone() {
return phone;
}
public String getCode() {
return code;
}
/**
* 获取请求的scopes
*/
public Set<String> getScopes() {
return this.scopes;
}
/**
* 获取请求中的 grant_type
*/
public AuthorizationGrantType getAuthorizationGrantType() {
return this.authorizationGrantType;
}
/**
* 获取请求中的附加参数
*/
public Map<String, Object> getAdditionalParameters() {
return this.additionalParameters;
}
}
SmsAuthenticationConverter
public final class SmsAuthenticationConverter implements AuthenticationConverter {
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationServerConfigurationConsent.GRANT_TYPE_SMS_CODE.equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
//OAuth2AuthorizationUtils 是copy 源码中存在的
MultiValueMap<String, String> parameters = OAuth2AuthorizationUtils.getParameters(request);
// scope (OPTIONAL)
Set<String> scopes = null;
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) &&
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
OAuth2AuthorizationUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE, null);
}
if (StringUtils.hasText(scope)) {
scopes = new HashSet<>(
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
// phone (REQUIRED) 手机号
String phone = parameters.getFirst(AuthorizationServerConfigurationConsent.OAUTH2_PARAMETER_NAME_PHONE);
if (!StringUtils.hasText(phone) ||
parameters.get(AuthorizationServerConfigurationConsent.OAUTH2_PARAMETER_NAME_PHONE).size() != 1) {
OAuth2AuthorizationUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
"OAuth 2.0 Parameter: " + AuthorizationServerConfigurationConsent.OAUTH2_PARAMETER_NAME_PHONE,
null);
}
// sms_code (REQUIRED) 验证码必填
String smsCode = parameters.getFirst(AuthorizationServerConfigurationConsent.OAUTH2_PARAMETER_NAME_SMS_CODE);
if (!StringUtils.hasText(smsCode) ||
parameters.get(AuthorizationServerConfigurationConsent.OAUTH2_PARAMETER_NAME_SMS_CODE).size() != 1) {
OAuth2AuthorizationUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
"OAuth 2.0 Parameter: " + AuthorizationServerConfigurationConsent.OAUTH2_PARAMETER_NAME_SMS_CODE,
null);
}
//扩展参数
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE)) {
additionalParameters.put(key, value.get(0));
}
});
return new SmsAuthenticationToken(new AuthorizationGrantType(AuthorizationServerConfigurationConsent.GRANT_TYPE_SMS_CODE),
clientPrincipal,
scopes,
phone,
smsCode,
additionalParameters);
}
}
SmsAuthenticationProvider
@Slf4j public final class SmsAuthenticationProvider implements AuthenticationProvider { private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1"; private OAuth2AuthorizationService authorizationService; private OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator; private AuthenticationManager authenticationManager;//暂时没有用到 如果额外加一个 短信验证码的 Provider 参考 DaoAuthenticationProvider private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN); @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsAuthenticationToken smsAuthenticationToken = (SmsAuthenticationToken) authentication; OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthorizationUtils.getAuthenticatedClientElseThrowInvalidClient(smsAuthenticationToken); RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); if (log.isTraceEnabled()) { log.trace("Retrieved registered client"); } if (registeredClient == null) { throw new OAuth2AuthenticationException("client_id not exist"); } try { //todo 验证码验证逻辑 //验证码 String code = smsAuthenticationToken.getCode(); if (!StringUtils.hasText(code)) { throw new OAuth2AuthenticationException("验证码不能为空!"); } //todo 暂时先写000000 ,发送验证码的我们还没有写的 if (!code.equals("000000")) { throw new OAuth2AuthenticationException("验证码:【" + code + "】已过期!"); } Authentication smsCodeValidAuthentication = smsAuthenticationToken; smsAuthenticationToken.setAuthenticated(true); // @formatter:off DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder() .registeredClient(registeredClient) .principal(smsCodeValidAuthentication) .authorizationServerContext(AuthorizationServerContextHolder.getContext()) .authorizedScopes(smsAuthenticationToken.getScopes()) .authorizationGrantType(smsAuthenticationToken.getAuthorizationGrantType()) .authorizationGrant(smsAuthenticationToken); // @formatter:on OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization .withRegisteredClient(registeredClient) .principalName(smsCodeValidAuthentication.getName()) .authorizationGrantType(smsAuthenticationToken.getAuthorizationGrantType()) .authorizedScopes(smsAuthenticationToken.getScopes()); // ----- Access token ----- OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build(); OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext); if (generatedAccessToken == null) { OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the access token.", ERROR_URI); throw new OAuth2AuthenticationException(error); } if (log.isTraceEnabled()) { log.trace("Generated access token"); } OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); if (generatedAccessToken instanceof ClaimAccessor) { authorizationBuilder.token(accessToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims())) .attribute(Principal.class.getName(), smsCodeValidAuthentication); } else { authorizationBuilder.accessToken(accessToken); } // ----- Refresh token ----- OAuth2RefreshToken refreshToken = null; if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) && // Do not issue refresh token to public client !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) { tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build(); OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext); if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) { OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the refresh token.", ERROR_URI); throw new OAuth2AuthenticationException(error); } if (log.isTraceEnabled()) { log.trace("Generated refresh token"); } refreshToken = (OAuth2RefreshToken) generatedRefreshToken; authorizationBuilder.refreshToken(refreshToken); } // ----- ID token ----- OidcIdToken idToken; if (smsAuthenticationToken.getScopes().contains(OidcScopes.OPENID)) { // @formatter:off tokenContext = tokenContextBuilder .tokenType(ID_TOKEN_TOKEN_TYPE) .authorization(authorizationBuilder.build()) // ID token customizer may need access to the access token and/or refresh token .build(); // @formatter:on OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext); if (!(generatedIdToken instanceof Jwt)) { OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the ID token.", ERROR_URI); throw new OAuth2AuthenticationException(error); } if (log.isTraceEnabled()) { log.trace("Generated id token"); } idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(), generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims()); authorizationBuilder.token(idToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims())); } else { idToken = null; } OAuth2Authorization authorization = authorizationBuilder.build(); this.authorizationService.save(authorization); if (log.isTraceEnabled()) { log.trace("Saved authorization"); } Map<String, Object> additionalParameters = Collections.emptyMap(); if (idToken != null) { additionalParameters = new HashMap<>(); additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue()); } if (log.isTraceEnabled()) { log.trace("Authenticated token request"); } return new OAuth2AccessTokenAuthenticationToken( registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters); } catch (Exception e) { OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, e.getMessage(), ERROR_URI); throw new OAuth2AuthenticationException(error); } } @Override public boolean supports(Class<?> authentication) { return SmsAuthenticationToken.class.isAssignableFrom(authentication); } public void setTokenGenerator(OAuth2TokenGenerator<?> tokenGenerator) { Assert.notNull(tokenGenerator, "tokenGenerator cannot be null"); this.tokenGenerator = tokenGenerator; } public void setAuthenticationManager(AuthenticationManager authenticationManager) { Assert.notNull(authorizationService, "authenticationManager cannot be null"); this.authenticationManager = authenticationManager; } public void setAuthorizationService(OAuth2AuthorizationService authorizationService) { Assert.notNull(authorizationService, "authorizationService cannot be null"); this.authorizationService = authorizationService; } }OAuth2AuthorizationService authorizationService;
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
AuthenticationManager authenticationManager; 以上3个属性,是在HttpSecuritybuild 之后才能够从 HttpSecurity 中获取到,所以SmsAuthenticationProvider的注入就要在HttpSecuritybuild 之后再添加到 SecurityFilterChain 中。
SmsAuthenticationProvider、SmsAuthenticationConverter 注入SecurityFilterChain中
在AuthorizationServerConfig 的 SecurityFilterChain @Bean 中添加,因为你看官方给的demo中就是这里添加,再有一个原因就是AuthorizationServerConfig中的SecurityFilterChain就是 oauth2 的配置。
AuthorizationServerConfig 的完整配置
@Configuration(proxyBeanMethods = false) public class AuthorizationServerConfig { private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";//这个是授权页 @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain( HttpSecurity http, RegisteredClientRepository registeredClientRepository, AuthorizationServerSettings authorizationServerSettings) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); DeviceClientAuthenticationConverter deviceClientAuthenticationConverter = new DeviceClientAuthenticationConverter( authorizationServerSettings.getDeviceAuthorizationEndpoint()); DeviceClientAuthenticationProvider deviceClientAuthenticationProvider = new DeviceClientAuthenticationProvider(registeredClientRepository); // @formatter:off http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .deviceAuthorizationEndpoint(deviceAuthorizationEndpoint -> deviceAuthorizationEndpoint.verificationUri("/activate") ) .deviceVerificationEndpoint(deviceVerificationEndpoint -> deviceVerificationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI) ) .clientAuthentication(clientAuthentication -> clientAuthentication .authenticationConverter(deviceClientAuthenticationConverter) .authenticationProvider(deviceClientAuthenticationProvider) ) .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)) .oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0 // @formatter:on // @formatter:off http .exceptionHandling(exceptions -> exceptions .defaultAuthenticationEntryPointFor( new LoginUrlAuthenticationEntryPoint(AuthorizationServerConfigurationConsent.LOGIN_PAGE_URL), new MediaTypeRequestMatcher(MediaType.TEXT_HTML) ) ) .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer.jwt(Customizer.withDefaults())); // @formatter:on //sms off SmsAuthenticationConverter smsAuthenticationConverter = new SmsAuthenticationConverter(); SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider(); //sms on http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .tokenEndpoint(tokenEndpoint-> tokenEndpoint.accessTokenRequestConverter(smsAuthenticationConverter) .authenticationProvider(smsAuthenticationProvider)//选择追加的方式 ); DefaultSecurityFilterChain build = http.build(); this.initAuthenticationProviderFiled(http, smsAuthenticationProvider, smsCodeValidAuthenticationProvider); return build; } @Bean public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository); } @Bean public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository); } @Bean public OAuth2TokenCustomizer<JwtEncodingContext> idTokenCustomizer() { return new FederatedIdentityIdTokenCustomizer(); } @Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build(); } /** * 初始化 Provider 中的 OAuth2TokenGenerator、AuthenticationManager、OAuth2AuthorizationService 属性 * @param http * @param providers */ private void initAuthenticationProviderFiled(HttpSecurity http, AuthenticationProvider... providers) { //http.build 之后 Spring Security过滤器链才完整构建 这个时候才能从中获取到以下想要获取到的class实例(其他方法后面有时间再试一试) OAuth2TokenGenerator<?> tokenGenerator = http.getSharedObject(OAuth2TokenGenerator.class); AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class); for (AuthenticationProvider provider : providers) { if (provider instanceof SmsAuthenticationProvider smsAuthenticationProvider) { //这个class需要用到依赖 smsAuthenticationProvider.setAuthorizationService(authorizationService); smsAuthenticationProvider.setTokenGenerator(tokenGenerator); smsAuthenticationProvider.setAuthenticationManager(authenticationManager); } } } }
手机号+验证码模式演示
获取token
刷新token
扩展也是很so easy,不要把spring想的过于复杂了。