🍉Spring Authorization Server (6) 授权服务器 授权类型扩展

1,202 阅读5分钟

架构版本
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_credentials

Spring 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); 

授权码模式OAuth2AuthorizationCodeAuthenticationConverterOAuth2AuthorizationCodeAuthenticationTokenOAuth2AuthorizationCodeAuthenticationProvider是怎么玩的,我们就怎么玩。

那我现在理解搞定这 OAuth2AuthorizationCodeAuthenticationConverterOAuth2AuthorizationCodeAuthenticationTokenOAuth2AuthorizationCodeAuthenticationProvider 3个就行了。

手机号+短信验证码模式扩展

参考授权码模式,就是需要 AuthenticationConverterAuthenticationTokenAuthenticationProvider 对于的实现

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个属性,是在 HttpSecurity build 之后才能够从 HttpSecurity 中获取到,所以 SmsAuthenticationProvider 的注入就要在 HttpSecurity build 之后再添加到 SecurityFilterChain 中。

SmsAuthenticationProvider、SmsAuthenticationConverter 注入SecurityFilterChain中

AuthorizationServerConfigSecurityFilterChain @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

img_6day_1.png 刷新token

img_6day_2.png

扩展也是很so easy,不要把spring想的过于复杂了。