操作方法:使用社交登录认证

本指南展示如何为 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。在使用提供程序设置应用程序时,输入此值作为重定向 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 客户端依赖项

首先,添加以下依赖项

  • 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 为例,配置以下属性

application.yml
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)设置提供程序 URL、客户端 ID 和客户端密钥的推荐方法。有关更多信息,请参阅 Spring Boot 参考中的 外部化配置

此简单示例演示了典型的配置,但某些提供商需要额外的配置。有关配置 ClientRegistration 的更多信息,请参阅 Spring Security 参考中的 Spring Boot 属性映射

配置身份验证

最后,要配置 Spring Authorization Server 以便使用社交登录提供商进行身份验证,您可以使用 oauth2Login() 而不是 formLogin()。您还可以通过使用 AuthenticationEntryPoint 配置 exceptionHandling(),将未经身份验证的用户自动重定向到提供商。

继续我们的 前面的示例,使用 @Configuration 配置 Spring Security,如下例所示

配置 OAuth 2.0 登录
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 为重定向到 OAuth 2.0 登录端点 配置 AuthenticationEntryPoint
3 用于 身份验证 的 Spring Security 过滤器链。
4 为身份验证配置 OAuth 2.0 登录

如果您在 开始 时配置了 UserDetailsService,则现在可以将其删除。

高级用例

演示授权服务器示例 展示了用于联合身份提供商的高级配置选项。从以下用例中进行选择,查看每个用例的示例

在数据库中捕获用户

以下示例 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();
}