進階組態
HttpSecurity.oauth2Login()
為自訂 OAuth 2.0 登入提供了許多組態選項。主要的組態選項依其協定端點對應項分組。
例如,oauth2Login().authorizationEndpoint()
允許組態授權端點,而 oauth2Login().tokenEndpoint()
允許組態權杖端點。
以下程式碼顯示範例
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorization -> authorization
...
)
.redirectionEndpoint(redirection -> redirection
...
)
.tokenEndpoint(token -> token
...
)
.userInfoEndpoint(userInfo -> userInfo
...
)
);
return http.build();
}
}
@Configuration
@EnableWebSecurity
class OAuth2LoginSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
oauth2Login {
authorizationEndpoint {
...
}
redirectionEndpoint {
...
}
tokenEndpoint {
...
}
userInfoEndpoint {
...
}
}
}
return http.build()
}
}
oauth2Login()
DSL 的主要目標是與規格中定義的命名緊密對齊。
OAuth 2.0 授權框架將 協定端點 定義如下
授權流程使用兩個授權伺服器端點 (HTTP 資源)
-
授權端點:用戶端使用此端點透過使用者代理程式重新導向從資源擁有者取得授權。
-
權杖端點:用戶端使用此端點交換授權許可以取得存取權杖,通常使用用戶端驗證。
授權流程也使用一個用戶端端點
-
重新導向端點:授權伺服器使用此端點傳回回應,其中包含透過資源擁有者使用者代理程式傳回給用戶端的授權憑證。
OpenID Connect Core 1.0 規格將 UserInfo 端點 定義如下
UserInfo 端點是 OAuth 2.0 保護資源,可傳回關於已驗證最終使用者的宣告。為了取得關於最終使用者的請求宣告,用戶端透過使用透過 OpenID Connect 驗證取得的存取權杖,向 UserInfo 端點發出請求。這些宣告通常由 JSON 物件表示,其中包含宣告的名稱值組集合。
以下程式碼顯示 oauth2Login()
DSL 的完整組態選項
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.clientRegistrationRepository(this.clientRegistrationRepository())
.authorizedClientRepository(this.authorizedClientRepository())
.authorizedClientService(this.authorizedClientService())
.loginPage("/login")
.authorizationEndpoint(authorization -> authorization
.baseUri(this.authorizationRequestBaseUri())
.authorizationRequestRepository(this.authorizationRequestRepository())
.authorizationRequestResolver(this.authorizationRequestResolver())
)
.redirectionEndpoint(redirection -> redirection
.baseUri(this.authorizationResponseBaseUri())
)
.tokenEndpoint(token -> token
.accessTokenResponseClient(this.accessTokenResponseClient())
)
.userInfoEndpoint(userInfo -> userInfo
.userAuthoritiesMapper(this.userAuthoritiesMapper())
.userService(this.oauth2UserService())
.oidcUserService(this.oidcUserService())
)
);
return http.build();
}
}
@Configuration
@EnableWebSecurity
class OAuth2LoginSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
oauth2Login {
clientRegistrationRepository = clientRegistrationRepository()
authorizedClientRepository = authorizedClientRepository()
authorizedClientService = authorizedClientService()
loginPage = "/login"
authorizationEndpoint {
baseUri = authorizationRequestBaseUri()
authorizationRequestRepository = authorizationRequestRepository()
authorizationRequestResolver = authorizationRequestResolver()
}
redirectionEndpoint {
baseUri = authorizationResponseBaseUri()
}
tokenEndpoint {
accessTokenResponseClient = accessTokenResponseClient()
}
userInfoEndpoint {
userAuthoritiesMapper = userAuthoritiesMapper()
userService = oauth2UserService()
oidcUserService = oidcUserService()
}
}
}
return http.build()
}
}
除了 oauth2Login()
DSL 之外,也支援 XML 組態。
以下程式碼顯示 安全性命名空間 中可用的完整組態選項
<http>
<oauth2-login client-registration-repository-ref="clientRegistrationRepository"
authorized-client-repository-ref="authorizedClientRepository"
authorized-client-service-ref="authorizedClientService"
authorization-request-repository-ref="authorizationRequestRepository"
authorization-request-resolver-ref="authorizationRequestResolver"
access-token-response-client-ref="accessTokenResponseClient"
user-authorities-mapper-ref="userAuthoritiesMapper"
user-service-ref="oauth2UserService"
oidc-user-service-ref="oidcUserService"
login-processing-url="/login/oauth2/code/*"
login-page="/login"
authentication-success-handler-ref="authenticationSuccessHandler"
authentication-failure-handler-ref="authenticationFailureHandler"
jwt-decoder-factory-ref="jwtDecoderFactory"/>
</http>
以下章節將更詳細地介紹每個可用的組態選項
OAuth 2.0 登入頁面
預設情況下,OAuth 2.0 登入頁面由 DefaultLoginPageGeneratingFilter
自動產生。預設登入頁面會顯示每個已組態的 OAuth 用戶端,並以其 ClientRegistration.clientName
作為連結,此連結能夠啟動授權請求 (或 OAuth 2.0 登入)。
為了讓 |
每個 OAuth 用戶端的連結目的地預設為以下
OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{registrationId}"
以下行顯示範例
<a href="/oauth2/authorization/google">Google</a>
若要覆寫預設登入頁面,請組態 oauth2Login().loginPage()
和 (選擇性) oauth2Login().authorizationEndpoint().baseUri()
。
以下清單顯示範例
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.loginPage("/login/oauth2")
...
.authorizationEndpoint(authorization -> authorization
.baseUri("/login/oauth2/authorization")
...
)
);
return http.build();
}
}
@Configuration
@EnableWebSecurity
class OAuth2LoginSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
oauth2Login {
loginPage = "/login/oauth2"
authorizationEndpoint {
baseUri = "/login/oauth2/authorization"
}
}
}
return http.build()
}
}
<http>
<oauth2-login login-page="/login/oauth2"
...
/>
</http>
您需要提供一個 |
如先前所述,組態 以下行顯示範例
|
重新導向端點
重新導向端點由授權伺服器使用,透過資源擁有者使用者代理程式將授權回應 (其中包含授權憑證) 傳回給用戶端。
OAuth 2.0 登入利用授權碼授與。因此,授權憑證是授權碼。 |
預設授權回應 baseUri
(重新導向端點) 為 /login/oauth2/code/*
,定義於 OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI
中。
如果您想要自訂授權回應 baseUri
,請如下組態
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.redirectionEndpoint(redirection -> redirection
.baseUri("/login/oauth2/callback/*")
...
)
);
return http.build();
}
}
@Configuration
@EnableWebSecurity
class OAuth2LoginSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
oauth2Login {
redirectionEndpoint {
baseUri = "/login/oauth2/callback/*"
}
}
}
return http.build()
}
}
<http>
<oauth2-login login-processing-url="/login/oauth2/callback/*"
...
/>
</http>
您也需要確保 以下清單顯示範例
|
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
的實作並組態它,如下所示
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userAuthoritiesMapper(this.userAuthoritiesMapper())
...
)
);
return http.build();
}
private 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
@EnableWebSecurity
class OAuth2LoginSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
oauth2Login {
userInfoEndpoint {
userAuthoritiesMapper = userAuthoritiesMapper()
}
}
}
return http.build()
}
private 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
}
}
<http>
<oauth2-login user-authorities-mapper-ref="userAuthoritiesMapper"
...
/>
</http>
或者,您可以註冊 GrantedAuthoritiesMapper
@Bean
以使其自動應用於組態,如下所示
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(withDefaults());
return http.build();
}
@Bean
public GrantedAuthoritiesMapper userAuthoritiesMapper() {
...
}
}
@Configuration
@EnableWebSecurity
class OAuth2LoginSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
oauth2Login { }
}
return http.build()
}
@Bean
fun userAuthoritiesMapper(): GrantedAuthoritiesMapper {
...
}
}
使用 OAuth2UserService 的委派式策略
與使用 GrantedAuthoritiesMapper
相比,此策略更進階。但是,它也更靈活,因為它可以讓您存取 OAuth2UserRequest
和 OAuth2User
(當使用 OAuth 2.0 UserService 時) 或 OidcUserRequest
和 OidcUser
(當使用 OpenID Connect 1.0 UserService 時)。
OAuth2UserRequest
(和 OidcUserRequest
) 讓您可以存取相關聯的 OAuth2AccessToken
,這在委派者需要在它可以對應使用者的自訂權限之前,從受保護資源提取權限資訊的情況下非常有用。
以下範例示範如何使用 OpenID Connect 1.0 UserService 實作和組態委派式策略
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(this.oidcUserService())
...
)
);
return http.build();
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
// Delegate to the default implementation for loading a user
OidcUser oidcUser = delegate.loadUser(userRequest);
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 oidcUser;
};
}
}
@Configuration
@EnableWebSecurity
class OAuth2LoginSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
oauth2Login {
userInfoEndpoint {
oidcUserService = oidcUserService()
}
}
}
return http.build()
}
@Bean
fun oidcUserService(): OAuth2UserService<OidcUserRequest, OidcUser> {
val delegate = OidcUserService()
return OAuth2UserService { userRequest ->
// Delegate to the default implementation for loading a user
val oidcUser = delegate.loadUser(userRequest)
val accessToken = userRequest.accessToken
val mappedAuthorities = HashSet<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()
if (StringUtils.hasText(userNameAttributeName)) {
DefaultOidcUser(mappedAuthorities, oidcUser.idToken, oidcUser.userInfo, userNameAttributeName)
} else {
DefaultOidcUser(mappedAuthorities, oidcUser.idToken, oidcUser.userInfo)
}
}
}
}
<http>
<oauth2-login oidc-user-service-ref="oidcUserService"
...
/>
</http>
OAuth 2.0 UserService
DefaultOAuth2UserService
是 OAuth2UserService
的實作,支援標準 OAuth 2.0 提供者。
|
當在 UserInfo 端點請求使用者屬性時,DefaultOAuth2UserService
使用 RestOperations
實例。
如果您需要自訂 UserInfo 請求的預先處理,您可以提供 DefaultOAuth2UserService.setRequestEntityConverter()
和自訂 Converter<OAuth2UserRequest, RequestEntity<?>>
。預設實作 OAuth2UserRequestEntityConverter
建置 UserInfo 請求的 RequestEntity
表示法,預設情況下在 Authorization
標頭中設定 OAuth2AccessToken
。
另一方面,如果您需要自訂 UserInfo 回應的後續處理,您需要提供 DefaultOAuth2UserService.setRestOperations()
和自訂組態的 RestOperations
。預設 RestOperations
組態如下
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
OAuth2ErrorResponseErrorHandler
是一個 ResponseErrorHandler
,可以處理 OAuth 2.0 錯誤 (400 Bad Request)。它使用 OAuth2ErrorHttpMessageConverter
將 OAuth 2.0 錯誤參數轉換為 OAuth2Error
。
無論您是自訂 DefaultOAuth2UserService
還是提供您自己的 OAuth2UserService
實作,您都需要如下組態它
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userService(this.oauth2UserService())
...
)
);
return http.build();
}
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
...
}
}
@Configuration
@EnableWebSecurity
class OAuth2LoginSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
oauth2Login {
userInfoEndpoint {
userService = oauth2UserService()
// ...
}
}
}
return http.build()
}
private fun oauth2UserService(): OAuth2UserService<OAuth2UserRequest, OAuth2User> {
// ...
}
}
OpenID Connect 1.0 UserService
OidcUserService
是 OAuth2UserService
的實作,支援 OpenID Connect 1.0 提供者。
當在 UserInfo 端點請求使用者屬性時,OidcUserService
利用 DefaultOAuth2UserService
。
如果您需要自訂 UserInfo 請求的預先處理或 UserInfo 回應的後續處理,您需要提供 OidcUserService.setOauth2UserService()
和自訂組態的 DefaultOAuth2UserService
。
無論您是自訂 OidcUserService
還是為 OpenID Connect 1.0 提供者提供您自己的 OAuth2UserService
實作,您都需要如下組態它
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(this.oidcUserService())
...
)
);
return http.build();
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
...
}
}
@Configuration
@EnableWebSecurity
class OAuth2LoginSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
oauth2Login {
userInfoEndpoint {
oidcUserService = oidcUserService()
// ...
}
}
}
return http.build()
}
private fun oidcUserService(): OAuth2UserService<OidcUserRequest, OidcUser> {
// ...
}
}
ID 權杖簽章驗證
OpenID Connect 1.0 驗證引入了 ID 權杖,這是一種安全權杖,包含關於最終使用者透過授權伺服器驗證的宣告 (當用戶端使用時)。
ID 權杖表示為 JSON Web 權杖 (JWT),且必須使用 JSON Web 簽章 (JWS) 簽署。
OidcIdTokenDecoderFactory
提供用於 OidcIdToken
簽章驗證的 JwtDecoder
。預設演算法為 RS256
,但在用戶端註冊期間指派時可能會有所不同。在這些情況下,您可以組態解析器以傳回為特定用戶端指派的預期 JWS 演算法。
JWS 演算法解析器是一個 Function
,它接受 ClientRegistration
並傳回用戶端的預期 JwsAlgorithm
,例如 SignatureAlgorithm.RS256
或 MacAlgorithm.HS256
以下程式碼示範如何組態 OidcIdTokenDecoderFactory
@Bean
,使其預設為所有 ClientRegistration
實例的 MacAlgorithm.HS256
-
Java
-
Kotlin
@Bean
public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
OidcIdTokenDecoderFactory idTokenDecoderFactory = new OidcIdTokenDecoderFactory();
idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256);
return idTokenDecoderFactory;
}
@Bean
fun idTokenDecoderFactory(): JwtDecoderFactory<ClientRegistration?> {
val idTokenDecoderFactory = OidcIdTokenDecoderFactory()
idTokenDecoderFactory.setJwsAlgorithmResolver { MacAlgorithm.HS256 }
return idTokenDecoderFactory
}
對於基於 MAC 的演算法 (例如 |
如果為 OpenID Connect 1.0 驗證組態了多個 |
然後,您可以繼續組態 登出