核心模型 / 元件

RegisteredClient

RegisteredClient 是授權伺服器上註冊的客戶端表示。客戶端必須先在授權伺服器上註冊,然後才能啟動授權許可流程,例如 authorization_codeclient_credentials

在客戶端註冊期間,客戶端會被分配一個唯一的客戶端識別符號,一個(可選的)客戶端金鑰(取決於客戶端型別),以及與其唯一客戶端識別符號相關的元資料。客戶端的元資料範圍很廣,從面向人類的顯示字串(如客戶端名稱)到特定於協議流程的專案(如有效重定向 URI 列表)。

Spring Security OAuth2 Client 支援中相應的客戶端註冊模型是 ClientRegistration

客戶端的主要目的是請求訪問受保護資源。客戶端首先透過向授權伺服器認證並出示授權許可來請求訪問令牌。授權伺服器會認證客戶端和授權許可,如果它們有效,則頒發訪問令牌。客戶端現在可以通過出示訪問令牌向資源伺服器請求受保護資源。

以下示例展示瞭如何配置一個允許執行 authorization_code 許可流程以請求訪問令牌的 RegisteredClient

RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
	.clientId("client-a")
	.clientSecret("{noop}secret")   (1)
	.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
	.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
	.redirectUri("http://127.0.0.1:8080/authorized")
	.scope("scope-a")
	.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
	.build();
1 {noop} 表示 Spring Security PasswordEncoderNoOpPasswordEncoder ID。

Spring Security OAuth2 Client 支援中的相應配置是

spring:
  security:
    oauth2:
      client:
        registration:
          client-a:
            provider: spring
            client-id: client-a
            client-secret: secret
            authorization-grant-type: authorization_code
            redirect-uri: "http://127.0.0.1:8080/authorized"
            scope: scope-a
        provider:
          spring:
            issuer-uri: https://:9000

一個 RegisteredClient 擁有與其唯一客戶端識別符號相關的元資料(屬性),定義如下

public class RegisteredClient implements Serializable {
	private String id;  (1)
	private String clientId;    (2)
	private Instant clientIdIssuedAt;   (3)
	private String clientSecret;    (4)
	private Instant clientSecretExpiresAt;  (5)
	private String clientName;  (6)
	private Set<ClientAuthenticationMethod> clientAuthenticationMethods;    (7)
	private Set<AuthorizationGrantType> authorizationGrantTypes;    (8)
	private Set<String> redirectUris;   (9)
	private Set<String> postLogoutRedirectUris; (10)
	private Set<String> scopes; (11)
	private ClientSettings clientSettings;  (12)
	private TokenSettings tokenSettings;    (13)

	...

}
1 id: 唯一標識 RegisteredClient 的 ID。
2 clientId: 客戶端識別符號。
3 clientIdIssuedAt: 客戶端識別符號頒發的時間。
4 clientSecret: 客戶端金鑰。該值應使用 Spring Security PasswordEncoder 進行編碼。
5 clientSecretExpiresAt: 客戶端金鑰過期的時間。
6 clientName: 用於客戶端的描述性名稱。該名稱可能在某些場景中使用,例如在同意頁面顯示客戶端名稱時。
7 clientAuthenticationMethods: 客戶端可以使用的認證方法。支援的值有 client_secret_basicclient_secret_postprivate_key_jwtclient_secret_jwtnone (公共客戶端)
8 authorizationGrantTypes: 客戶端可以使用的授權許可型別。支援的值有 authorization_codeclient_credentialsrefresh_tokenurn:ietf:params:oauth:grant-type:device_codeurn:ietf:params:oauth:grant-type:token-exchange
9 redirectUris: 客戶端在基於重定向的流程(例如 authorization_code 許可)中可以使用的已註冊重定向 URI
10 postLogoutRedirectUris: 客戶端在登出後可以使用的重定向 URI。
11 scopes: 客戶端被允許請求的範圍 (scope)。
12 clientSettings: 客戶端的自定義設定,例如要求 PKCE、要求授權同意等。
13 tokenSettings: 頒發給客戶端的 OAuth2 令牌的自定義設定,例如訪問/重新整理令牌的生命週期、是否重用重新整理令牌等。

RegisteredClientRepository

RegisteredClientRepository 是用於註冊新客戶端和查詢現有客戶端的核心元件。其他元件在執行特定協議流程時(如客戶端認證、授權許可處理、令牌內省、動態客戶端註冊等)會使用它。

提供了 InMemoryRegisteredClientRepositoryJdbcRegisteredClientRepository 兩種 RegisteredClientRepository 實現。InMemoryRegisteredClientRepository 實現將 RegisteredClient 例項儲存在記憶體中,建議在開發和測試期間使用。JdbcRegisteredClientRepository 是一個 JDBC 實現,它使用 JdbcOperations 持久化 RegisteredClient 例項。

RegisteredClientRepository 是一個必需的元件。

以下示例展示瞭如何註冊一個 RegisteredClientRepository@Bean

@Bean
public RegisteredClientRepository registeredClientRepository() {
	List<RegisteredClient> registrations = ...
	return new InMemoryRegisteredClientRepository(registrations);
}

或者,您可以透過 OAuth2AuthorizationServerConfigurer 配置 RegisteredClientRepository

@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
	OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
			OAuth2AuthorizationServerConfigurer.authorizationServer();

	http
		.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
		.with(authorizationServerConfigurer, (authorizationServer) ->
			authorizationServer
				.registeredClientRepository(registeredClientRepository)
		)
	    ...

	return http.build();
}
在同時應用多個配置選項時,OAuth2AuthorizationServerConfigurer 非常有用。

OAuth2Authorization

OAuth2Authorization 是 OAuth2 授權的表示,它儲存了資源所有者(或客戶端本身,在使用 client_credentials 授權許可型別時)授予客戶端授權的相關狀態。

Spring Security OAuth2 Client 支援中相應的授權模型是 OAuth2AuthorizedClient

授權許可流程成功完成後,會建立一個 OAuth2Authorization,並關聯一個 OAuth2AccessToken、一個(可選的)OAuth2RefreshToken,以及特定於已執行授權許可型別的附加狀態。

OAuth2Authorization 關聯的 OAuth2Token 例項會根據授權許可型別的不同而變化。

對於 OAuth2 authorization_code 許可,會關聯一個 OAuth2AuthorizationCode、一個 OAuth2AccessToken 和一個(可選的)OAuth2RefreshToken

對於 OpenID Connect 1.0 authorization_code 許可,會關聯一個 OAuth2AuthorizationCode、一個 OidcIdToken、一個 OAuth2AccessToken 和一個(可選的)OAuth2RefreshToken

對於 OAuth2 client_credentials 許可,僅關聯一個 OAuth2AccessToken

OAuth2Authorization 及其屬性定義如下

public class OAuth2Authorization implements Serializable {
	private String id;  (1)
	private String registeredClientId;  (2)
	private String principalName;   (3)
	private AuthorizationGrantType authorizationGrantType;  (4)
	private Set<String> authorizedScopes;   (5)
	private Map<Class<? extends OAuth2Token>, Token<?>> tokens; (6)
	private Map<String, Object> attributes; (7)

	...

}
1 id: 唯一標識 OAuth2Authorization 的 ID。
2 registeredClientId: 唯一標識 RegisteredClient 的 ID。
3 principalName: 資源所有者(或客戶端)的主體名稱 (principal name)。
4 authorizationGrantType: 使用的 AuthorizationGrantType
5 authorizedScopes: 為客戶端授權的範圍 (scope) 的 Set
6 tokens: 特定於已執行授權許可型別的 OAuth2Token 例項(及相關元資料)。
7 attributes: 特定於已執行授權許可型別的附加屬性,例如已認證的 PrincipalOAuth2AuthorizationRequest 等。

OAuth2Authorization 及其關聯的 OAuth2Token 例項都有設定的生命週期。新頒發的 OAuth2Token 是活動的,過期或失效(被撤銷)後變為非活動狀態。當所有關聯的 OAuth2Token 例項都非活動時,OAuth2Authorization(隱式地)變為非活動狀態。每個 OAuth2Token 都儲存在 OAuth2Authorization.Token 中,該物件提供 isExpired()isInvalidated()isActive() 等訪問方法。

OAuth2Authorization.Token 還提供 getClaims() 方法,用於返回與 OAuth2Token 關聯的宣告 (claim)(如果有)。

OAuth2AuthorizationService

OAuth2AuthorizationService 是用於儲存新授權和查詢現有授權的核心元件。其他元件在執行特定協議流程時(例如客戶端認證、授權許可處理、令牌內省、令牌撤銷、動態客戶端註冊等)會使用它。

提供了 InMemoryOAuth2AuthorizationServiceJdbcOAuth2AuthorizationService 兩種 OAuth2AuthorizationService 實現。InMemoryOAuth2AuthorizationService 實現將 OAuth2Authorization 例項儲存在記憶體中,建議在開發和測試期間使用。JdbcOAuth2AuthorizationService 是一個 JDBC 實現,它使用 JdbcOperations 持久化 OAuth2Authorization 例項。

OAuth2AuthorizationService 是一個可選元件,預設為 InMemoryOAuth2AuthorizationService

以下示例展示瞭如何註冊一個 OAuth2AuthorizationService@Bean

@Bean
public OAuth2AuthorizationService authorizationService() {
	return new InMemoryOAuth2AuthorizationService();
}

或者,您可以透過 OAuth2AuthorizationServerConfigurer 配置 OAuth2AuthorizationService

@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
	OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
			OAuth2AuthorizationServerConfigurer.authorizationServer();

	http
		.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
		.with(authorizationServerConfigurer, (authorizationServer) ->
			authorizationServer
				.authorizationService(authorizationService)
		)
	    ...

	return http.build();
}
在同時應用多個配置選項時,OAuth2AuthorizationServerConfigurer 非常有用。

OAuth2AuthorizationConsentOAuth2 授權請求流程中的授權“同意”(決定)表示,例如 authorization_code 許可,它包含了資源所有者授予客戶端的許可權。

在授權客戶端訪問時,資源所有者可能只授予客戶端請求的許可權子集。典型用例是 authorization_code 許可流程,其中客戶端請求範圍 (scope),而資源所有者授予(或拒絕)對所請求範圍的訪問。

OAuth2 授權請求流程完成後,會建立(或更新)一個 OAuth2AuthorizationConsent,並將授予的許可權與客戶端和資源所有者關聯起來。

OAuth2AuthorizationConsent 及其屬性定義如下

public final class OAuth2AuthorizationConsent implements Serializable {
	private final String registeredClientId;    (1)
	private final String principalName; (2)
	private final Set<GrantedAuthority> authorities;    (3)

	...

}
1 registeredClientId: 唯一標識 RegisteredClient 的 ID。
2 principalName: 資源所有者的主體名稱 (principal name)。
3 authorities: 資源所有者授予客戶端的許可權 (authority)。一個許可權可以代表一個範圍 (scope)、一個宣告 (claim)、一個許可 (permission)、一個角色 (role) 等。

OAuth2AuthorizationConsentService 是用於儲存新授權同意和查詢現有授權同意的核心元件。它主要由實現 OAuth2 授權請求流程的元件使用,例如 authorization_code 許可。

提供了 InMemoryOAuth2AuthorizationConsentServiceJdbcOAuth2AuthorizationConsentService 兩種 OAuth2AuthorizationConsentService 實現。InMemoryOAuth2AuthorizationConsentService 實現將 OAuth2AuthorizationConsent 例項儲存在記憶體中,建議在開發和測試期間使用。JdbcOAuth2AuthorizationConsentService 是一個 JDBC 實現,它使用 JdbcOperations 持久化 OAuth2AuthorizationConsent 例項。

OAuth2AuthorizationConsentService 是一個可選元件,預設為 InMemoryOAuth2AuthorizationConsentService

以下示例展示瞭如何註冊一個 OAuth2AuthorizationConsentService@Bean

@Bean
public OAuth2AuthorizationConsentService authorizationConsentService() {
	return new InMemoryOAuth2AuthorizationConsentService();
}

或者,您可以透過 OAuth2AuthorizationServerConfigurer 配置 OAuth2AuthorizationConsentService

@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
	OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
			OAuth2AuthorizationServerConfigurer.authorizationServer();

	http
		.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
		.with(authorizationServerConfigurer, (authorizationServer) ->
			authorizationServer
				.authorizationConsentService(authorizationConsentService)
		)
	    ...

	return http.build();
}
在同時應用多個配置選項時,OAuth2AuthorizationServerConfigurer 非常有用。

OAuth2TokenContext

OAuth2TokenContext 是一個上下文物件,它儲存了與 OAuth2Token 相關的資訊,供 OAuth2TokenGeneratorOAuth2TokenCustomizer 使用。

OAuth2TokenContext 提供以下訪問器 (accessor)

public interface OAuth2TokenContext extends Context {

	default RegisteredClient getRegisteredClient() ...  (1)

	default <T extends Authentication> T getPrincipal() ... (2)

	default AuthorizationServerContext getAuthorizationServerContext() ...    (3)

	@Nullable
	default OAuth2Authorization getAuthorization() ...  (4)

	default Set<String> getAuthorizedScopes() ...   (5)

	default OAuth2TokenType getTokenType() ...  (6)

	default AuthorizationGrantType getAuthorizationGrantType() ...  (7)

	default <T extends Authentication> T getAuthorizationGrant() ...    (8)

	...

}
1 getRegisteredClient(): 與授權許可關聯的 RegisteredClient
2 getPrincipal(): 資源所有者(或客戶端)的 Authentication 例項。
3 getAuthorizationServerContext(): 包含授權伺服器執行時環境資訊的 AuthorizationServerContext 物件。
4 getAuthorization(): 與授權許可關聯的 OAuth2Authorization
5 getAuthorizedScopes(): 為客戶端授權的範圍 (scope)。
6 getTokenType(): 要生成的 OAuth2TokenType。支援的值有 codeaccess_tokenrefresh_tokenid_token
7 getAuthorizationGrantType(): 與授權許可關聯的 AuthorizationGrantType
8 getAuthorizationGrant(): 處理授權許可的 AuthenticationProvider 使用的 Authentication 例項。

OAuth2TokenGenerator

OAuth2TokenGenerator 負責根據提供的 OAuth2TokenContext 中包含的資訊生成 OAuth2Token

生成的 OAuth2Token 主要取決於 OAuth2TokenContext 中指定的 OAuth2TokenType 型別。

例如,當 OAuth2TokenType 的值為

  • code 時,生成 OAuth2AuthorizationCode

  • access_token 時,生成 OAuth2AccessToken

  • refresh_token 時,生成 OAuth2RefreshToken

  • id_token 時,生成 OidcIdToken

此外,生成的 OAuth2AccessToken 的格式會根據為 RegisteredClient 配置的 TokenSettings.getAccessTokenFormat() 而變化。如果格式是 OAuth2TokenFormat.SELF_CONTAINED(預設),則生成一個 Jwt。如果格式是 OAuth2TokenFormat.REFERENCE,則生成一個“不透明”令牌 (opaque token)。

最後,如果生成的 OAuth2Token 包含一組宣告並實現了 ClaimAccessor 介面,則可以透過 OAuth2Authorization.Token.getClaims() 訪問這些宣告。

OAuth2TokenGenerator 主要由實現授權許可處理的元件使用,例如 authorization_codeclient_credentialsrefresh_token

提供的實現有 OAuth2AccessTokenGeneratorOAuth2RefreshTokenGeneratorJwtGeneratorOAuth2AccessTokenGenerator 生成“不透明”(OAuth2TokenFormat.REFERENCE)訪問令牌,JwtGenerator 生成 JwtOAuth2TokenFormat.SELF_CONTAINED)。

OAuth2TokenGenerator 是一個可選元件,預設為一個由 OAuth2AccessTokenGeneratorOAuth2RefreshTokenGenerator 組成的 DelegatingOAuth2TokenGenerator
如果註冊了 JwtEncoder@BeanJWKSource<SecurityContext>@Bean,則 DelegatingOAuth2TokenGenerator 中還會額外包含一個 JwtGenerator

OAuth2TokenGenerator 提供了極大的靈活性,因為它支援任何自定義的 access_tokenrefresh_token 令牌格式。

以下示例展示瞭如何註冊一個 OAuth2TokenGenerator@Bean

@Bean
public OAuth2TokenGenerator<?> tokenGenerator() {
	JwtEncoder jwtEncoder = ...
	JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder);
	OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
	OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
	return new DelegatingOAuth2TokenGenerator(
			jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
}

或者,您可以透過 OAuth2AuthorizationServerConfigurer 配置 OAuth2TokenGenerator

@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
	OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
			OAuth2AuthorizationServerConfigurer.authorizationServer();

	http
		.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
		.with(authorizationServerConfigurer, (authorizationServer) ->
			authorizationServer
				.tokenGenerator(tokenGenerator)
		)
	    ...

	return http.build();
}
在同時應用多個配置選項時,OAuth2AuthorizationServerConfigurer 非常有用。

OAuth2TokenCustomizer

OAuth2TokenCustomizer 提供了自定義 OAuth2Token 屬性的能力,這些屬性可以透過提供的 OAuth2TokenContext 訪問。它由 OAuth2TokenGenerator 使用,以便在生成 OAuth2Token 之前自定義其屬性。

宣告泛型型別為 OAuth2TokenClaimsContext(實現 OAuth2TokenContext)的 OAuth2TokenCustomizer<OAuth2TokenClaimsContext> 提供了自定義“不透明”OAuth2AccessToken 宣告 (claim) 的能力。OAuth2TokenClaimsContext.getClaims() 提供對 OAuth2TokenClaimsSet.Builder 的訪問,允許新增、替換和移除宣告。

以下示例展示瞭如何實現一個 OAuth2TokenCustomizer<OAuth2TokenClaimsContext> 並將其與 OAuth2AccessTokenGenerator 一起配置

@Bean
public OAuth2TokenGenerator<?> tokenGenerator() {
	JwtEncoder jwtEncoder = ...
	JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder);
	OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
	accessTokenGenerator.setAccessTokenCustomizer(accessTokenCustomizer());
	OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
	return new DelegatingOAuth2TokenGenerator(
			jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
}

@Bean
public OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer() {
	return context -> {
		OAuth2TokenClaimsSet.Builder claims = context.getClaims();
		// Customize claims

	};
}
如果未將 OAuth2TokenGenerator 作為 @Bean 提供,或未透過 OAuth2AuthorizationServerConfigurer 進行配置,則一個 OAuth2TokenCustomizer<OAuth2TokenClaimsContext>@Bean 將自動與 OAuth2AccessTokenGenerator 一起配置。

宣告泛型型別為 JwtEncodingContext(實現 OAuth2TokenContext)的 OAuth2TokenCustomizer<JwtEncodingContext> 提供了自定義 Jwt 的頭部 (header) 和宣告 (claim) 的能力。JwtEncodingContext.getJwsHeader() 提供對 JwsHeader.Builder 的訪問,允許新增、替換和移除頭部。JwtEncodingContext.getClaims() 提供對 JwtClaimsSet.Builder 的訪問,允許新增、替換和移除宣告。

以下示例展示瞭如何實現一個 OAuth2TokenCustomizer<JwtEncodingContext> 並將其與 JwtGenerator 一起配置

@Bean
public OAuth2TokenGenerator<?> tokenGenerator() {
	JwtEncoder jwtEncoder = ...
	JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder);
	jwtGenerator.setJwtCustomizer(jwtCustomizer());
	OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
	OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
	return new DelegatingOAuth2TokenGenerator(
			jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
}

@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
	return context -> {
		JwsHeader.Builder headers = context.getJwsHeader();
		JwtClaimsSet.Builder claims = context.getClaims();
		if (context.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) {
			// Customize headers/claims for access_token

		} else if (context.getTokenType().getValue().equals(OidcParameterNames.ID_TOKEN)) {
			// Customize headers/claims for id_token

		}
	};
}
如果未將 OAuth2TokenGenerator 作為 @Bean 提供,或未透過 OAuth2AuthorizationServerConfigurer 進行配置,則一個 OAuth2TokenCustomizer<JwtEncodingContext>@Bean 將自動與 JwtGenerator 一起配置。

SessionRegistry

如果啟用了 OpenID Connect 1.0,則使用 SessionRegistry 例項來跟蹤已認證的會話。與 OAuth2 授權端點關聯的 SessionAuthenticationStrategy 的預設實現使用 SessionRegistry 來註冊新的已認證會話。

如果沒有註冊 SessionRegistry@Bean,將使用預設實現 SessionRegistryImpl
如果註冊了 SessionRegistry@Bean 並且它是 SessionRegistryImpl 的例項,則也應該註冊一個 HttpSessionEventPublisher@Bean,因為它負責將會話生命週期事件(例如 SessionDestroyedEvent)通知給 SessionRegistryImpl,從而提供移除 SessionInformation 例項的能力。

當終端使用者請求登出時,OpenID Connect 1.0 登出端點使用 SessionRegistry 查詢與已認證終端使用者關聯的 SessionInformation 來執行登出操作。

如果正在使用 Spring Security 的併發會話控制功能,建議註冊一個 SessionRegistry@Bean,以確保它在 Spring Security 的併發會話控制和 Spring Authorization Server 的登出功能之間共享。

以下示例展示瞭如何註冊一個 SessionRegistry@Bean 和一個 HttpSessionEventPublisher@BeanSessionRegistryImpl 所需)

@Bean
public SessionRegistry sessionRegistry() {
	return new SessionRegistryImpl();
}

@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
	return new HttpSessionEventPublisher();
}