進階組態
OAuth 2.0 授權框架將 協定端點 定義如下
授權過程利用兩個授權伺服器端點 (HTTP 資源)
-
授權端點:用戶端使用此端點透過使用者代理重新導向,從資源擁有者取得授權。
-
權杖端點:用戶端使用此端點交換授權許可以取得存取權杖,通常需要用戶端驗證。
以及一個用戶端端點
-
重新導向端點:授權伺服器使用此端點透過資源擁有者使用者代理,將包含授權憑證的回應傳回給用戶端。
OpenID Connect Core 1.0 規範將 UserInfo 端點 定義如下
UserInfo 端點是一個 OAuth 2.0 保護資源,會傳回關於已驗證最終使用者的宣告。為了取得關於最終使用者的請求宣告,用戶端會使用透過 OpenID Connect 驗證取得的存取權杖,向 UserInfo 端點發出請求。這些宣告通常以 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") ,以便能夠呈現自訂登入頁面。 |
如先前所述,組態 以下程式碼行顯示一個範例
|
重新導向端點
重新導向端點由授權伺服器用於將授權回應 (其中包含授權憑證) 傳回給用戶端,透過資源擁有者使用者代理。
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 回應的後續處理,您將需要使用自訂組態的 WebClient
來提供 DefaultReactiveOAuth2UserService.setWebClient()
。
無論您是自訂 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 提供者。
當在 UserInfo 端點請求使用者屬性時,OidcReactiveOAuth2UserService
會利用 DefaultReactiveOAuth2UserService
。
如果您需要自訂 UserInfo 請求的預先處理和/或 UserInfo 回應的後續處理,您將需要使用自訂組態的 ReactiveOAuth2UserService
來提供 OidcReactiveOAuth2UserService.setOauth2UserService()
。
無論您是自訂 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
提供用於 OidcIdToken
簽章驗證的 ReactiveJwtDecoder
。預設演算法為 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 ,以判斷要傳回的演算法。 |
然後,您可以繼續組態登出。