如何:使用社群登入驗證
本指南說明如何使用社群登入提供者 (例如 Google、GitHub 等) 設定 Spring Authorization Server 以進行驗證。本指南的目的是示範如何使用 OAuth 2.0 登入取代表單登入。
Spring Authorization Server 建構於 Spring Security 之上,我們將在本指南中通篇使用 Spring Security 的概念。 |
向社群登入提供者註冊
首先,您需要使用您選擇的社群登入提供者設定應用程式。常見的提供者包括
依照您提供者的步驟操作,直到系統要求您指定重新導向 URI。若要設定重新導向 URI,請選擇一個 registrationId
(例如 google
、my-client
或您希望使用的任何其他唯一識別碼),您將使用它來設定 Spring Security **和**您的提供者。
registrationId 是 Spring Security 中 ClientRegistration 的唯一識別碼。預設的重新導向 URI 範本是 {baseUrl}/login/oauth2/code/{registrationId} 。如需更多資訊,請參閱 Spring Security 參考文件中的設定重新導向 URI。 |
例如,在本機連接埠 9000 上使用 registrationId 為 google 進行測試,您的重新導向 URI 將為 localhost:9000/login/oauth2/code/google 。在設定您提供者的應用程式時,輸入此值作為重新導向 URI。 |
在您完成社群登入提供者的設定程序後,您應該已取得憑證 (用戶端 ID 和用戶端密鑰)。此外,您需要參考提供者的文件,並記下下列值
-
授權 URI:用於在提供者處啟動
authorization_code
流程的端點。 -
令牌 URI:用於交換
authorization_code
以取得access_token
,以及選擇性地取得id_token
的端點。 -
JWK 集合 URI:用於取得金鑰以驗證 JWT 簽章的端點,當
id_token
可用時,這是必需的。 -
使用者資訊 URI:用於取得使用者資訊的端點,當
id_token
不可用時,這是必需的。 -
使用者名稱屬性:
id_token
或使用者資訊回應中包含使用者使用者名稱的宣告。
設定 OAuth 2.0 登入
在您註冊社群登入提供者後,您可以繼續設定 Spring Security 以進行 OAuth 2.0 登入。
新增 OAuth2 用戶端相依性
首先,新增下列相依性
-
Maven
-
Gradle
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
註冊用戶端
接下來,使用先前取得的值設定 ClientRegistration
。以 Okta 為例,設定下列屬性
okta:
base-url: ${OKTA_BASE_URL}
spring:
security:
oauth2:
client:
registration:
my-client:
provider: okta
client-id: ${OKTA_CLIENT_ID}
client-secret: ${OKTA_CLIENT_SECRET}
scope:
- openid
- profile
- email
provider:
okta:
authorization-uri: ${okta.base-url}/oauth2/v1/authorize
token-uri: ${okta.base-url}/oauth2/v1/token
user-info-uri: ${okta.base-url}/oauth2/v1/userinfo
jwk-set-uri: ${okta.base-url}/oauth2/v1/keys
user-name-attribute: sub
上述範例中的 registrationId 是 my-client 。 |
上述範例示範了設定提供者 URL、用戶端 ID 和用戶端密鑰的**建議**方式,使用環境變數 (OKTA_BASE_URL 、OKTA_CLIENT_ID 和 OKTA_CLIENT_SECRET )。如需更多資訊,請參閱 Spring Boot 參考文件中的外部化組態。 |
這個簡單的範例示範了典型的組態,但某些提供者將需要額外的組態。如需更多關於設定 ClientRegistration
的資訊,請參閱 Spring Security 參考文件中的Spring Boot 屬性對應。
設定驗證
最後,若要設定 Spring Authorization Server 使用社群登入提供者進行驗證,您可以使用 oauth2Login()
而不是 formLogin()
。您也可以透過使用 AuthenticationEntryPoint
設定 exceptionHandling()
,自動將未經驗證的使用者重新導向至提供者。
繼續我們的先前範例,使用 @Configuration
設定 Spring Security,如下列範例所示
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean (1)
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
http
// Redirect to the OAuth 2.0 Login endpoint when not authenticated
// from the authorization endpoint
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor( (2)
new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/my-client"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// Accept access tokens for User Info and/or Client Registration
.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean (3)
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
// OAuth2 Login handles the redirect to the OAuth 2.0 Login endpoint
// from the authorization server filter chain
.oauth2Login(Customizer.withDefaults()); (4)
return http.build();
}
}
1 | 用於協定端點的 Spring Security 篩選器鏈。 |
2 | 設定 AuthenticationEntryPoint 以重新導向至 OAuth 2.0 登入端點。 |
3 | 用於驗證的 Spring Security 篩選器鏈。 |
4 | 設定 OAuth 2.0 登入以進行驗證。 |
如果您在開始使用時設定了 UserDetailsService
,您現在可以將其移除。
進階使用案例
示範授權伺服器範例示範了用於聯合身份提供者的進階組態選項。從下列使用案例中選擇以查看每個案例的範例
-
我想要在資料庫中擷取使用者
-
我想要將宣告對應到 ID 令牌
在資料庫中擷取使用者
下列範例 AuthenticationSuccessHandler
使用自訂組件,在使用者首次登入時將其擷取到本機資料庫中
FederatedIdentityAuthenticationSuccessHandler
import java.io.IOException;
import java.util.function.Consumer;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
public final class FederatedIdentityAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final AuthenticationSuccessHandler delegate = new SavedRequestAwareAuthenticationSuccessHandler();
private Consumer<OAuth2User> oauth2UserHandler = (user) -> {};
private Consumer<OidcUser> oidcUserHandler = (user) -> this.oauth2UserHandler.accept(user);
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (authentication instanceof OAuth2AuthenticationToken) {
if (authentication.getPrincipal() instanceof OidcUser) {
this.oidcUserHandler.accept((OidcUser) authentication.getPrincipal());
} else if (authentication.getPrincipal() instanceof OAuth2User) {
this.oauth2UserHandler.accept((OAuth2User) authentication.getPrincipal());
}
}
this.delegate.onAuthenticationSuccess(request, response, authentication);
}
public void setOAuth2UserHandler(Consumer<OAuth2User> oauth2UserHandler) {
this.oauth2UserHandler = oauth2UserHandler;
}
public void setOidcUserHandler(Consumer<OidcUser> oidcUserHandler) {
this.oidcUserHandler = oidcUserHandler;
}
}
使用上述 AuthenticationSuccessHandler
,您可以插入自己的 Consumer<OAuth2User>
,它可以將使用者擷取到資料庫或其他資料儲存區中,以用於聯合帳號連結或 JIT 帳號佈建等概念。以下是一個簡單地將使用者儲存在記憶體中的範例
UserRepositoryOAuth2UserHandler
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import org.springframework.security.oauth2.core.user.OAuth2User;
public final class UserRepositoryOAuth2UserHandler implements Consumer<OAuth2User> {
private final UserRepository userRepository = new UserRepository();
@Override
public void accept(OAuth2User user) {
// Capture user in a local data store on first authentication
if (this.userRepository.findByName(user.getName()) == null) {
System.out.println("Saving first-time user: name=" + user.getName() + ", claims=" + user.getAttributes() + ", authorities=" + user.getAuthorities());
this.userRepository.save(user);
}
}
static class UserRepository {
private final Map<String, OAuth2User> userCache = new ConcurrentHashMap<>();
public OAuth2User findByName(String name) {
return this.userCache.get(name);
}
public void save(OAuth2User oauth2User) {
this.userCache.put(oauth2User.getName(), oauth2User);
}
}
}
將宣告對應到 ID 令牌
下列範例 OAuth2TokenCustomizer
將使用者的宣告從驗證提供者對應到 Spring Authorization Server 產生的 id_token
FederatedIdentityIdTokenCustomizer
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
public final class FederatedIdentityIdTokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {
private static final Set<String> ID_TOKEN_CLAIMS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
IdTokenClaimNames.ISS,
IdTokenClaimNames.SUB,
IdTokenClaimNames.AUD,
IdTokenClaimNames.EXP,
IdTokenClaimNames.IAT,
IdTokenClaimNames.AUTH_TIME,
IdTokenClaimNames.NONCE,
IdTokenClaimNames.ACR,
IdTokenClaimNames.AMR,
IdTokenClaimNames.AZP,
IdTokenClaimNames.AT_HASH,
IdTokenClaimNames.C_HASH
)));
@Override
public void customize(JwtEncodingContext context) {
if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
Map<String, Object> thirdPartyClaims = extractClaims(context.getPrincipal());
context.getClaims().claims(existingClaims -> {
// Remove conflicting claims set by this authorization server
existingClaims.keySet().forEach(thirdPartyClaims::remove);
// Remove standard id_token claims that could cause problems with clients
ID_TOKEN_CLAIMS.forEach(thirdPartyClaims::remove);
// Add all other claims directly to id_token
existingClaims.putAll(thirdPartyClaims);
});
}
}
private Map<String, Object> extractClaims(Authentication principal) {
Map<String, Object> claims;
if (principal.getPrincipal() instanceof OidcUser) {
OidcUser oidcUser = (OidcUser) principal.getPrincipal();
OidcIdToken idToken = oidcUser.getIdToken();
claims = idToken.getClaims();
} else if (principal.getPrincipal() instanceof OAuth2User) {
OAuth2User oauth2User = (OAuth2User) principal.getPrincipal();
claims = oauth2User.getAttributes();
} else {
claims = Collections.emptyMap();
}
return new HashMap<>(claims);
}
}
您可以透過將此自訂器發佈為 @Bean
來設定 Spring Authorization Server 使用它,如下列範例所示
FederatedIdentityIdTokenCustomizer
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> idTokenCustomizer() {
return new FederatedIdentityIdTokenCustomizer();
}