核心模型 / 组件

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: http://localhost: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();
}