跳到主要内容

操作指南:使用社交登录进行身份验证

DeepSeek V3 中英对照 How-to: Authenticate using Social Login

本指南展示了如何为 Spring Authorization Server 配置社交登录提供商(例如 Google、GitHub 等)以实现身份验证。本指南旨在演示如何用 OAuth 2.0 登录 替换表单登录

备注

Spring Authorization Server 构建于 Spring Security 之上,在本指南中我们将贯穿使用 Spring Security 的概念。

通过社交登录提供商注册

要开始使用,你需要在你选择的社会化登录提供商处设置一个应用程序。常见的提供商包括:

按照您所选提供商的步骤操作,直到系统要求您指定重定向 URI。要设置重定向 URI,请选择一个 registrationId(例如 googlemy-client 或您希望使用的任何其他唯一标识符),您将使用此标识符来配置 Spring Security 以及您的提供商。

备注

registrationId 是 Spring Security 中 ClientRegistration 的唯一标识符。默认的重定向 URI 模板是 {baseUrl}/login/oauth2/code/{registrationId}。更多信息请参阅 Spring Security 参考文档中的 设置重定向 URI

提示

例如,在本地端口 9000 上使用 registrationIdgoogle 进行测试时,你的重定向 URI 应为 [localhost:9000/login/oauth2/code/google](http://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 登录

一旦你注册了社交登录提供商,就可以继续为 OAuth 2.0 登录配置 Spring Security。

添加 OAuth2 客户端依赖

首先,添加以下依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

注册客户端

接下来,使用之前获取的值配置 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
备注

上述示例中的 registrationIdmy-client

提示

上述示例展示了使用环境变量(OKTA_BASE_URLOKTA_CLIENT_IDOKTA_CLIENT_SECRET)来设置 Provider URL、Client ID 和 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.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 {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();

http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.oidc(Customizer.withDefaults()) // Enable OpenID Connect 1.0
)
.authorizeHttpRequests((authorize) ->
authorize
.anyRequest().authenticated()
)
// 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)
)
);

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();
}

}

如果你在入门指南中配置了 UserDetailsService,现在可以将其移除。

高级使用场景

demo authorization server sample 演示了用于联合身份提供者的高级配置选项。请从以下用例中选择,查看每个用例的示例:

在数据库中捕获用户

以下示例 AuthenticationSuccessHandler 使用自定义组件,在用户首次登录时将其捕获到本地数据库中:

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 oidcUser) {
this.oidcUserHandler.accept(oidcUser);
} else if (authentication.getPrincipal() instanceof OAuth2User oauth2User) {
this.oauth2UserHandler.accept(oauth2User);
}
}

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>,用于将用户信息捕获到数据库或其他数据存储中,以实现联邦账户关联或即时账户配置等概念。以下是一个简单地将用户存储在内存中的示例:

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

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) {
OidcIdToken idToken = oidcUser.getIdToken();
claims = idToken.getClaims();
} else if (principal.getPrincipal() instanceof OAuth2User oauth2User) {
claims = oauth2User.getAttributes();
} else {
claims = Collections.emptyMap();
}

return new HashMap<>(claims);
}

}

你可以通过将自定义器发布为 @Bean 来配置 Spring Authorization Server 使用它,如下例所示:

@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> idTokenCustomizer() {
return new FederatedIdentityIdTokenCustomizer();
}