高级配置

OAuth 2.0 授权框架将 协议端点 定义如下

授权过程使用了两个授权服务器端点(HTTP 资源)

  • 授权端点(Authorization Endpoint):客户端通过用户代理重定向,从资源所有者处获取授权时使用。

  • 令牌端点(Token Endpoint):客户端使用授权许可换取访问令牌时使用,通常包含客户端认证。

以及一个客户端端点

  • 重定向端点(Redirection Endpoint):授权服务器通过资源所有者用户代理,将包含授权凭据的响应返回给客户端时使用。

OpenID Connect Core 1.0 规范将 UserInfo Endpoint 定义如下

UserInfo Endpoint 是一个 OAuth 2.0 保护资源,它返回有关已认证最终用户的声明。为了获取请求的最终用户声明,客户端使用通过 OpenID Connect 认证获得的访问令牌向 UserInfo Endpoint 发送请求。这些声明通常由一个 JSON 对象表示,该对象包含一组键值对用于表示声明。

ServerHttpSecurity.oauth2Login() 提供了许多配置选项,用于定制 OAuth 2.0 登录。

以下代码展示了 oauth2Login() DSL 的完整可用配置选项

OAuth2 登录配置选项
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

	@Bean
	SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
		http
			.oauth2Login(oauth2 -> oauth2
				.authenticationConverter(this.authenticationConverter())
				.authenticationMatcher(this.authenticationMatcher())
				.authenticationManager(this.authenticationManager())
				.authenticationSuccessHandler(this.authenticationSuccessHandler())
				.authenticationFailureHandler(this.authenticationFailureHandler())
				.clientRegistrationRepository(this.clientRegistrationRepository())
				.authorizedClientRepository(this.authorizedClientRepository())
				.authorizedClientService(this.authorizedClientService())
				.authorizationRequestResolver(this.authorizationRequestResolver())
				.authorizationRequestRepository(this.authorizationRequestRepository())
				.securityContextRepository(this.securityContextRepository())
			);

		return http.build();
	}
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            oauth2Login {
                authenticationConverter = authenticationConverter()
                authenticationMatcher = authenticationMatcher()
                authenticationManager = authenticationManager()
                authenticationSuccessHandler = authenticationSuccessHandler()
                authenticationFailureHandler = authenticationFailureHandler()
                clientRegistrationRepository = clientRegistrationRepository()
                authorizedClientRepository = authorizedClientRepository()
                authorizedClientService = authorizedClientService()
                authorizationRequestResolver = authorizationRequestResolver()
                authorizationRequestRepository = authorizationRequestRepository()
                securityContextRepository = securityContextRepository()
            }
        }

        return http.build()
    }
}

以下章节将详细介绍每个可用的配置选项

OAuth 2.0 登录页

默认情况下,OAuth 2.0 登录页由 LoginPageGeneratingWebFilter 自动生成。默认登录页会显示每个配置的 OAuth 客户端,并将其 ClientRegistration.clientName 作为链接,该链接可以发起授权请求(或 OAuth 2.0 登录)。

为了让 LoginPageGeneratingWebFilter 显示已配置 OAuth 客户端的链接,注册的 ReactiveClientRegistrationRepository 需要同时实现 Iterable<ClientRegistration> 接口。参考 InMemoryReactiveClientRegistrationRepository

每个 OAuth 客户端链接的默认目标地址如下

"/oauth2/authorization/{registrationId}"

以下行显示了一个示例

<a href="/oauth2/authorization/google">Google</a>

要覆盖默认登录页,请配置 exceptionHandling().authenticationEntryPoint() 和(可选地)oauth2Login().authorizationRequestResolver()

以下清单显示了一个示例

OAuth2 登录页配置
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			.exceptionHandling(exceptionHandling -> exceptionHandling
				.authenticationEntryPoint(new RedirectServerAuthenticationEntryPoint("/login/oauth2"))
			)
			.oauth2Login(oauth2 -> oauth2
				.authorizationRequestResolver(this.authorizationRequestResolver())
			);

		return http.build();
	}

	private ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver() {
		ServerWebExchangeMatcher authorizationRequestMatcher =
				new PathPatternParserServerWebExchangeMatcher(
						"/login/oauth2/authorization/{registrationId}");

		return new DefaultServerOAuth2AuthorizationRequestResolver(
				this.clientRegistrationRepository(), authorizationRequestMatcher);
	}

	...
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            exceptionHandling {
                authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/login/oauth2")
            }
            oauth2Login {
                authorizationRequestResolver = authorizationRequestResolver()
            }
        }

        return http.build()
    }

    private fun authorizationRequestResolver(): ServerOAuth2AuthorizationRequestResolver {
        val authorizationRequestMatcher: ServerWebExchangeMatcher = PathPatternParserServerWebExchangeMatcher(
            "/login/oauth2/authorization/{registrationId}"
        )

        return DefaultServerOAuth2AuthorizationRequestResolver(
            clientRegistrationRepository(), authorizationRequestMatcher
        )
    }

    ...
}
您需要提供一个 `@Controller`,并使用 `@RequestMapping("/login/oauth2")` 注解,该 Controller 能够渲染自定义登录页。

如前所述,配置 oauth2Login().authorizationRequestResolver() 是可选的。但是,如果您选择自定义它,请确保指向每个 OAuth 客户端的链接与通过 ServerWebExchangeMatcher 提供的模式匹配。

以下行显示了一个示例

<a href="/login/oauth2/authorization/google">Google</a>

重定向端点

重定向端点由授权服务器使用,通过资源所有者的用户代理将授权响应(包含授权凭据)返回给客户端。

OAuth 2.0 登录利用了授权码模式。因此,授权凭据就是授权码。

默认的授权响应重定向端点是 `/login/oauth2/code/{registrationId}`。

如果您想自定义授权响应重定向端点,请按照以下示例进行配置

重定向端点配置
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			.oauth2Login(oauth2 -> oauth2
				.authenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}"))
			);

		return http.build();
	}
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            oauth2Login {
                authenticationMatcher = PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}")
            }
        }

        return http.build()
    }
}

您还需要确保 ClientRegistration.redirectUri 与自定义的授权响应重定向端点匹配。

以下清单显示了一个示例

  • Java

  • Kotlin

return CommonOAuth2Provider.GOOGLE.getBuilder("google")
	.clientId("google-client-id")
	.clientSecret("google-client-secret")
	.redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}")
	.build();
return CommonOAuth2Provider.GOOGLE.getBuilder("google")
    .clientId("google-client-id")
    .clientSecret("google-client-secret")
    .redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}")
    .build()

UserInfo 端点

UserInfo 端点包含许多配置选项,如下面的子章节所述

映射用户权限

用户成功通过 OAuth 2.0 提供者认证后,OAuth2User.getAuthorities()(或 OidcUser.getAuthorities())将包含一个授权权限列表,该列表由 OAuth2UserRequest.getAccessToken().getScopes() 填充并以 SCOPE_ 为前缀。这些授权权限可以映射到一组新的 GrantedAuthority 实例,在完成认证时会提供给 OAuth2AuthenticationToken

OAuth2AuthenticationToken.getAuthorities() 用于授权请求,例如在 hasRole('USER')hasRole('ADMIN') 中。

映射用户权限时,有几个选项可供选择

使用 GrantedAuthoritiesMapper

GrantedAuthoritiesMapper 会接收一个授权权限列表,其中包含一个类型为 OAuth2UserAuthority 且权限字符串为 OAUTH2_USER 的特殊权限(或者类型为 OidcUserAuthority 且权限字符串为 OIDC_USER)。

注册一个 GrantedAuthoritiesMapper 的 `@Bean`,使其自动应用到配置中,示例如下

Granted Authorities Mapper 配置
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			...
			.oauth2Login(withDefaults());

		return http.build();
	}

	@Bean
	public GrantedAuthoritiesMapper userAuthoritiesMapper() {
		return (authorities) -> {
			Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

			authorities.forEach(authority -> {
				if (OidcUserAuthority.class.isInstance(authority)) {
					OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;

					OidcIdToken idToken = oidcUserAuthority.getIdToken();
					OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();

					// Map the claims found in idToken and/or userInfo
					// to one or more GrantedAuthority's and add it to mappedAuthorities

				} else if (OAuth2UserAuthority.class.isInstance(authority)) {
					OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;

					Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();

					// Map the attributes found in userAttributes
					// to one or more GrantedAuthority's and add it to mappedAuthorities

				}
			});

			return mappedAuthorities;
		};
	}
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            oauth2Login { }
        }

        return http.build()
    }

    @Bean
    fun userAuthoritiesMapper(): GrantedAuthoritiesMapper = GrantedAuthoritiesMapper { authorities: Collection<GrantedAuthority> ->
        val mappedAuthorities = emptySet<GrantedAuthority>()

        authorities.forEach { authority ->
            if (authority is OidcUserAuthority) {
                val idToken = authority.idToken
                val userInfo = authority.userInfo
                // Map the claims found in idToken and/or userInfo
                // to one or more GrantedAuthority's and add it to mappedAuthorities
            } else if (authority is OAuth2UserAuthority) {
                val userAttributes = authority.attributes
                // Map the attributes found in userAttributes
                // to one or more GrantedAuthority's and add it to mappedAuthorities
            }
        }

        mappedAuthorities
    }
}

基于委托策略结合 ReactiveOAuth2UserService

与使用 GrantedAuthoritiesMapper 相比,此策略更高级,但也更灵活,因为它允许您访问 OAuth2UserRequestOAuth2User(在使用 OAuth 2.0 UserService 时),或 OidcUserRequestOidcUser(在使用 OpenID Connect 1.0 UserService 时)。

OAuth2UserRequest(和 OidcUserRequest)允许您访问相关的 OAuth2AccessToken,这在 *委托方* 需要从受保护资源获取权限信息才能为用户映射自定义权限的情况下非常有用。

以下示例展示了如何使用 OpenID Connect 1.0 UserService 实现和配置基于委托的策略

ReactiveOAuth2UserService 配置
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			...
			.oauth2Login(withDefaults());

		return http.build();
	}

	@Bean
	public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
		final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();

		return (userRequest) -> {
			// Delegate to the default implementation for loading a user
			return delegate.loadUser(userRequest)
					.flatMap((oidcUser) -> {
						OAuth2AccessToken accessToken = userRequest.getAccessToken();
						Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

						// TODO
						// 1) Fetch the authority information from the protected resource using accessToken
						// 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities

						// 3) Create a copy of oidcUser but use the mappedAuthorities instead
						ProviderDetails providerDetails = userRequest.getClientRegistration().getProviderDetails();
						String userNameAttributeName = providerDetails.getUserInfoEndpoint().getUserNameAttributeName();
						if (StringUtils.hasText(userNameAttributeName)) {
							oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo(), userNameAttributeName);
						} else {
							oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
						}

						return Mono.just(oidcUser);
					});
		};
	}
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            oauth2Login { }
        }

        return http.build()
    }

    @Bean
    fun oidcUserService(): ReactiveOAuth2UserService<OidcUserRequest, OidcUser> {
        val delegate = OidcReactiveOAuth2UserService()

        return ReactiveOAuth2UserService { userRequest ->
            // Delegate to the default implementation for loading a user
            delegate.loadUser(userRequest)
                .flatMap { oidcUser ->
                    val accessToken = userRequest.accessToken
                    val mappedAuthorities = mutableSetOf<GrantedAuthority>()

                    // TODO
                    // 1) Fetch the authority information from the protected resource using accessToken
                    // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
                    // 3) Create a copy of oidcUser but use the mappedAuthorities instead
                    val providerDetails = userRequest.getClientRegistration().getProviderDetails()
                    val userNameAttributeName = providerDetails.getUserInfoEndpoint().getUserNameAttributeName()
                    val mappedOidcUser = if (StringUtils.hasText(userNameAttributeName)) {
                        DefaultOidcUser(mappedAuthorities, oidcUser.idToken, oidcUser.userInfo, userNameAttributeName)
                    } else {
                        DefaultOidcUser(mappedAuthorities, oidcUser.idToken, oidcUser.userInfo)
                    }

                    Mono.just(mappedOidcUser)
                }
        }
    }
}

OAuth 2.0 UserService

DefaultReactiveOAuth2UserServiceReactiveOAuth2UserService 的一个实现,支持标准的 OAuth 2.0 提供者。

ReactiveOAuth2UserService 从 UserInfo 端点(通过使用在授权流程中授予客户端的访问令牌)获取最终用户(资源所有者)的用户属性,并以 OAuth2User 的形式返回 AuthenticatedPrincipal

DefaultReactiveOAuth2UserService 在 UserInfo 端点请求用户属性时使用 WebClient

如果您需要自定义 UserInfo 请求的预处理和/或 UserInfo 响应的后处理,则需要向 DefaultReactiveOAuth2UserService.setWebClient() 提供一个自定义配置的 WebClient

无论您是自定义 DefaultReactiveOAuth2UserService 还是提供自己的 ReactiveOAuth2UserService 实现,都需要按照以下示例进行配置

  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			...
			.oauth2Login(withDefaults());

		return http.build();
	}

	@Bean
	public ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
		...
	}
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            oauth2Login { }
        }

        return http.build()
    }

    @Bean
    fun oauth2UserService(): ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> {
        // ...
    }
}

OpenID Connect 1.0 UserService

OidcReactiveOAuth2UserServiceReactiveOAuth2UserService 的一个实现,支持 OpenID Connect 1.0 提供者。

OidcReactiveOAuth2UserService 在 UserInfo 端点请求用户属性时,利用了 DefaultReactiveOAuth2UserService

如果您需要自定义 UserInfo 请求的预处理和/或 UserInfo 响应的后处理,则需要向 OidcReactiveOAuth2UserService.setOauth2UserService() 提供一个自定义配置的 ReactiveOAuth2UserService

无论您是自定义 OidcReactiveOAuth2UserService 还是为 OpenID Connect 1.0 提供者提供自己的 ReactiveOAuth2UserService 实现,都需要按照以下示例进行配置

  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			...
			.oauth2Login(withDefaults());

		return http.build();
	}

	@Bean
	public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
		...
	}
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            oauth2Login { }
        }

        return http.build()
    }

    @Bean
    fun oidcUserService(): ReactiveOAuth2UserService<OidcUserRequest, OidcUser> {
        // ...
    }
}

ID 令牌签名验证

OpenID Connect 1.0 认证引入了 ID 令牌,这是一种安全令牌,客户端使用时,其中包含授权服务器对最终用户进行认证的声明。

ID 令牌以 JSON Web Token (JWT) 的形式表示,并且 必须 使用 JSON Web Signature (JWS) 进行签名。

ReactiveOidcIdTokenDecoderFactory 提供一个 ReactiveJwtDecoder,用于 OidcIdToken 签名验证。默认算法是 RS256,但在客户端注册时可能会指定不同的算法。对于这些情况,可以配置一个解析器来返回为特定客户端分配的预期 JWS 算法。

JWS 算法解析器是一个 Function,它接受一个 ClientRegistration 并返回客户端预期的 JwsAlgorithm,例如 `SignatureAlgorithm.RS256` 或 `MacAlgorithm.HS256`

以下代码展示了如何配置 OidcIdTokenDecoderFactory 的 `@Bean`,使其对所有 ClientRegistration 默认使用 MacAlgorithm.HS256 算法

  • Java

  • Kotlin

@Bean
public ReactiveJwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
	ReactiveOidcIdTokenDecoderFactory idTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
	idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256);
	return idTokenDecoderFactory;
}
@Bean
fun idTokenDecoderFactory(): ReactiveJwtDecoderFactory<ClientRegistration> {
    val idTokenDecoderFactory = ReactiveOidcIdTokenDecoderFactory()
    idTokenDecoderFactory.setJwsAlgorithmResolver { MacAlgorithm.HS256 }
    return idTokenDecoderFactory
}
对于基于 MAC 的算法,如 HS256HS384HS512,与 client-id 对应的 client-secret 被用作对称密钥进行签名验证。
如果为 OpenID Connect 1.0 认证配置了多个 ClientRegistration,JWS 算法解析器可以评估提供的 ClientRegistration 来确定应返回哪个算法。

然后,您可以继续配置 登出