高级配置
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 的完整可用配置选项
-
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()
。
以下清单显示了一个示例
-
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 能够渲染自定义登录页。 |
如前所述,配置 以下行显示了一个示例
|
重定向端点
重定向端点由授权服务器使用,通过资源所有者的用户代理将授权响应(包含授权凭据)返回给客户端。
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()
}
}
您还需要确保 以下清单显示了一个示例
|
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`,使其自动应用到配置中,示例如下
-
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
相比,此策略更高级,但也更灵活,因为它允许您访问 OAuth2UserRequest
和 OAuth2User
(在使用 OAuth 2.0 UserService 时),或 OidcUserRequest
和 OidcUser
(在使用 OpenID Connect 1.0 UserService 时)。
OAuth2UserRequest
(和 OidcUserRequest
)允许您访问相关的 OAuth2AccessToken
,这在 *委托方* 需要从受保护资源获取权限信息才能为用户映射自定义权限的情况下非常有用。
以下示例展示了如何使用 OpenID Connect 1.0 UserService 实现和配置基于委托的策略
-
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
DefaultReactiveOAuth2UserService
是 ReactiveOAuth2UserService
的一个实现,支持标准的 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
OidcReactiveOAuth2UserService
是 ReactiveOAuth2UserService
的一个实现,支持 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 的算法,如 HS256 、HS384 或 HS512 ,与 client-id 对应的 client-secret 被用作对称密钥进行签名验证。 |
如果为 OpenID Connect 1.0 认证配置了多个 ClientRegistration ,JWS 算法解析器可以评估提供的 ClientRegistration 来确定应返回哪个算法。 |
然后,您可以继续配置 登出。