OAuth 2.0授权框架对协议端点的定义如下:
授权过程使用两个授权服务器端点(HTTP资源):
- 授权端点: 由客户端使用,通过用户代理重定向从资源所有者获得授权。
- 令牌端点: 由客户端使用,用于交换认证授权给访问令牌,通常与客户端身份验证一起使用。
以及一个客户端端点:
- 重定向端点: 授权服务器使用重定向端点通过资源所有者用户代理将包含授权凭证的响应返回给客户端。
OpenID Connect Core 1.0规范对UserInfo端点的定义如下:
UserInfo端点是一个OAuth 2.0保护资源,它返回关于经过身份验证的最终用户的声明。为了获得关于最终用户的请求,客户端使用通过OpenID连接身份验证获得的访问令牌向UserInfo端点发出请求。这些声明通常由一个JSON对象表示,该对象包含声明的名称-值对集合。
ServerHttpSecurity.oauth2Login()
提供了许多自定义OAuth 2.0登录的配置选项。
以下代码显示了oauth2Login()
DSL可用的完整配置选项:
@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();
}
}
下面的章节将详细介绍每个可用的配置选项:
OAuth 2.0登录页面
默认情况下,OAuth 2.0登录页面是由LoginPageGeneratingWebFilter
自动生成的。默认登录页面显示每个配置的OAuth客户端及其ClientRegistration.clientName
作为链接,它能够启动授权请求(或OAuth 2.0登录)。
为了让LoginPageGeneratingWebFilter
显示配置的OAuth客户端的链接,注册的ReactiveClientRegistrationRepository
还需要实现Iterable<ClientRegistration>
。参考InMemoryReactiveClientRegistrationRepository
。
每个OAuth客户端链接的目的地默认为: "/oauth2/authorization/{registrationId}"
要覆盖默认登录页面,请配置exceptionHandling().authenticationEntryPoint()
和(可选的)oauth2Login().authorizationRequestResolver()
。
下面的清单显示了一个示例:
package com.hz.ss.config;
import org.springframework.context.annotation.Bean;
...
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
@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())
.authenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}")));
return http.build();
}
private ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver() {
ServerWebExchangeMatcher authorizationRequestMatcher = new PathPatternParserServerWebExchangeMatcher("/login/oauth2/authorization/{registrationId}");
return new DefaultServerOAuth2AuthorizationRequestResolver(this.clientRegistrationRepository(), authorizationRequestMatcher);
}
@Bean
public ReactiveClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryReactiveClientRegistrationRepository(this.giteeClientRegistration());
}
/**
* 上一项目使用 application.properties 配置方式 这里我们使用编程方式来实现自定义OAuth提供者
*/
private ClientRegistration giteeClientRegistration() {
return ClientRegistration.withRegistrationId("gitee")
.clientId("26511dbc353a90b647fbab9e24341d3a36a5bd7cf261b846c41fbd4e8500dcdf")
.clientSecret("ae4a6d5697e962c1b4f8043ba74b99aa58e930143758abcf69c3d2f1a209ca47")
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
//.scope("openid", "profile", "email", "address", "phone")
.authorizationUri("https://gitee.com/oauth/authorize")
.tokenUri("https://gitee.com/oauth/token")
.userInfoUri("https://gitee.com/api/v5/user")
.userNameAttributeName(IdTokenClaimNames.SUB)
.clientName("Gitee")
.build();
}
}
重要: 您需要使用@RequestMapping("/login/oauth2")
来提供能够呈现自定义登录页面的@Controller
。
Tip: 如前所述,配置oauth2Login().authorizationRequestResolver()
是可选的。但是,如果您选择定制它,请确保到每个OAuth Client的链接与通过ServerWebExchangeMatcher
提供的模式匹配。例:上一节的/login/oauth/authorization/gitee
就需要改为/login/oauth2/authorization/gitee
重定向端点
授权服务器使用重定向端点通过资源所有者用户代理将授权响应(其中包含授权凭据)返回给客户端。
Tip:OAuth 2.0登录利用授权码授权。因此,授权凭证就是授权代码。
默认的授权响应重定向端点为/login/oauth2/code/{registrationId}
。
如果您想自定义授权响应重定向端点,请按照如下示例配置它:
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http.
...
oauth2Login(oauth2 -> oauth2.authenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}"))
);
return http.build();
}
}
在上面代码的基础上我们将/login/oauth2/code/{registrationId}
改为我们自定义的重定向URL/login/oauth2/callback/{registrationId}
,也是可以正常授权的。
重要:您还需要确保ClientRegistration.redirectUri
匹配自定义授权响应重定向端点。
用户信息端点
UserInfo端点包括许多配置选项,如下面的子部分所述:
映射用户权限
在用户成功地通过OAuth 2.0 提供者进行身份验证后,OAuth2User.getAuthorities()
(或OidcUser.getAuthorities()
)可能会映射到一组新的GrantedAuthority
实例,当完成身份验证时,这些实例将被提供给OAuth2AuthenticationToken
。
Tip:OAuth2AuthenticationToken.getAuthorities()
用于授权请求,例如hasRole('USER')
或hasRole('ADMIN')
。
当映射用户权限时,有两个选项可以选择:
- 使用GrantedAuthoritiesMapper [一]
- 基于ReactiveOAuth2UserService的委派策略 [二]
一、注册一个GrantedAuthoritiesMapper
@Bean
,让它自动应用到配置中,如下面的示例所示:
package com.hz.ss.config;
...
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {
// 自定义重定向,而不使用默认的 /login/oauth2/code/{registrationId}
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
...
.oauth2Login(oauth2 -> oauth2
.authorizationRequestResolver(this.authorizationRequestResolver())
.authenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}"))
);
return http.build();
}
private ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver() {
...
}
@Bean
public ReactiveClientRegistrationRepository clientRegistrationRepository() {
...
}
/**
* 上一项目使用 application.properties 配置方式 这里我们使用编程方式来实现自定义OAuth提供者
*/
private ClientRegistration giteeClientRegistration() {
return ClientRegistration.withRegistrationId("gitee")
...
// 这里的重定向地址我们不再使用默认的 URL 默认为/login/oauth2/code/{registrationId}
.redirectUriTemplate("{baseUrl}/login/oauth2/callback/{registrationId}")
...
.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();
// 将在idToken和/或userInfo中发现的声明映射到一个或多个GrantedAuthority,并将其添加到 mappedAuthorities
} else if (OAuth2UserAuthority.class.isInstance(authority)) {
OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
// 将userAttributes中的属性映射到一个或多个GrantedAuthority,并将其添加到mappedAuthorities
}
});
return mappedAuthorities;
};
}
}
二、与使用GrantedAuthortiesMapper
相比,这种策略是高级的,但是,它也更灵活,因为它允许您访问OAuth2UserRequest
和OAuth2User
(当使用OAuth 2.0 UserService时)或OidcUserRequest
和OidcUser
(当使用OpenID Connect 1.0 UserService时)。
OAuth2UserRequest
(和OidcUserRequest
)为您提供了对相关联的OAuth2AccessToken
的访问,当委托程序在映射用户的自定义权限之前需要从受保护的资源中获取权限信息时,这非常有用。
下面的例子展示了如何使用OpenID Connect 1.0 UserService实现和配置基于委托的策略:
@Bean
public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();
return (userRequest) -> {
// 委托给默认实现以加载用户
return delegate.loadUser(userRequest)
.flatMap((oidcUser) -> {
OAuth2AccessToken accessToken = userRequest.getAccessToken();
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
// 1) 使用accessToken从受保护的资源获取权限信息
// 2) 将权限信息映射到一个或多个GrantedAuthority,并将其添加到 mappedAuthority
// 3) 创建oidcUser的副本,但使用mappedAuthorities代替
oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
return Mono.just(oidcUser);
});
};
}
OAuth 2.0 用户服务
DefaultReactiveOAuth2UserService
是ReactiveOAuth2UserService
的一个实现,它支持标准的OAuth 2.0 Provider。
ReactiveOAuth2UserService
从UserInfo端点获得最终用户(资源所有者)的用户属性(通过在授权流中使用授予客户端的访问令牌),并以OAuth2User
的形式返回AuthenticatedPrincipal
。
DefaultReactiveOAuth2UserService
在UserInfo端点请求用户属性时使用WebClient
。
如果你需要自定义UserInfo Request的预处理[和/或]UserInfo Response的后处理,你需要提供DefaultReactiveOAuth2UserService.setWebClient()
和一个自定义配置的WebClient
。
无论你是自定义DefaultReactiveOAuth2UserService
还是提供你自己的ReactiveOAuth2UserService
实现,你都需要配置它,如下面的例子所示:
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
...
.oauth2Login(withDefaults());
return http.build();
}
@Bean
public ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
...
}
}
OpenID Connect 1.0 用户服务
OidcReactiveOAuth2UserService
是一个ReactiveOAuth2UserService
的实现,它支持OpenID Connect 1.0 Provider。
当请求UserInfo端点上的用户属性时,OidcReactiveOAuth2UserService
利用DefaultReactiveOAuth2UserService
。
如果你需要定制UserInfo请求的预处理和/或UserInfo响应的事后处理,你需要提供一个自定义配置的ReactiveOAuth2UserService.setoauth2userservice()
的ReactiveOAuth2UserService
。
无论你是定制OidcReactiveOAuth2UserService
还是为OpenID Connect 1.0 Provider’s提供你自己的ReactiveOAuth2UserService
实现,你都需要配置它,如下所示的示例:
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
...
.oauth2Login(withDefaults());
return http.build();
}
@Bean
public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
...
}
}
ID令牌签名验证
OpenID Connect 1.0认证引入了ID Token,这是一个安全令牌,包含了当客户端使用时关于授权服务器对终端用户进行认证的声明。
ID令牌被表示为JSON Web令牌(JWT),必须使用JSON Web签名(JWS)进行签名。
ReactiveOidcIdTokenDecoderFactory
提供了一个ReactiveJwtDecoder
用于OidcIdToken
签名验证。默认算法为RS256
,客户端注册时指定的算法可能不同。对于这些情况,可以将解析器配置为返回为特定客户端分配的预期JWS算法。
JWS算法解析器是一个接受ClientRegistration
并为客户端返回预期JwsAlgorithm
的Function
。例如:SignatureAlgorithm.RS256
或MacAlgorithm.HS256
。
下面的代码展示了如何将OidcIdTokenDecoderFactory
@Bean
配置所有ClientRegistration
为默认的MacAlgorithm.HS256
:
@Bean
public ReactiveJwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
ReactiveOidcIdTokenDecoderFactory idTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256);
return idTokenDecoderFactory;
}
Note:对于基于MAC的算法,如HS256
、HS384
或HS512
,使用与client-id
对应的client-secret
作为签名验证的对称密钥。
Tip:如果OpenID Connect 1.0认证配置了多个ClientRegistration
, JWS算法解析器可能会评估提供的ClientRegistration
,以确定返回哪种算法。
OpenID Connect 1.0注销
OpenID Connect Session Management 1.0允许使用客户端注销提供商的终端用户。可用的策略之一是RP-Initiated Logout。
如果OpenID提供者同时支持会话管理和发现,客户端可以从OpenID提供者的发现元数据中获取end_session_endpoint
URL
。这可以通过使用issuer-uri
配置ClientRegistration
来实现,如下所示:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
...
provider:
okta:
issuer-uri: https://dev-1234.oktapreview.com
\