高階配置
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 來確定應返回哪個演算法。 |
然後,您可以繼續配置 登出。