本指南介绍如何使用社交登录提供程序(如Google,GitHub等)配置Spring Authorization Server进行身份验证。 本指南的目的是演示如何将表单登录替换为 OAuth 2.0 登录。
Spring Authorization Server 是基于 Spring Security 构建的,我们将在本指南中使用 Spring Security 概念。 |
Spring Authorization Server 是基于 Spring Security 构建的,我们将在本指南中使用 Spring Security 概念。 |
向社交登录提供商注册
首先,您需要使用您选择的社交登录提供商设置一个应用程序。 常见的提供商包括:
按照提供商的步骤操作,直到系统要求您指定重定向 URI。
要设置重定向 URI,请选择一个(例如 ,或您想要的任何其他唯一标识符),您将使用它来配置 Spring Security 和您的提供商。registrationId
google
my-client
是 Spring Security 中的唯一标识符。默认的重定向 URI 模板是 。有关更多信息,请参阅 Spring Security 参考中的设置重定向 URI。registrationId ClientRegistration {baseUrl}/login/oauth2/code/{registrationId} |
例如,在端口上使用 的本地测试,则重定向 URI 将为 。在提供程序设置应用程序时,输入此值作为重定向 URI。9000 registrationId google localhost:9000/login/oauth2/code/google |
使用社交登录提供程序完成设置过程后,应已获得凭据(客户端 ID 和客户端密码)。 此外,还需要引用提供程序的文档并记下以下值:
-
授权 URI:用于在提供程序上启动流的终结点。
authorization_code
-
令牌 URI:用于交换 an 和 (可选) 的终结点。
authorization_code
access_token
id_token
-
JWK 设置 URI:用于获取用于验证 JWT 签名的密钥的端点,当 JWT 可用时,这是必需的。
id_token
-
用户信息 URI:用于获取用户信息的终结点,当 URI 不可用时,这是必需的。
id_token
-
用户名属性:或包含用户用户名的“用户信息响应”中的声明。
id_token
是 Spring Security 中的唯一标识符。默认的重定向 URI 模板是 。有关更多信息,请参阅 Spring Security 参考中的设置重定向 URI。registrationId ClientRegistration {baseUrl}/login/oauth2/code/{registrationId} |
例如,在端口上使用 的本地测试,则重定向 URI 将为 。在提供程序设置应用程序时,输入此值作为重定向 URI。9000 registrationId google localhost:9000/login/oauth2/code/google |
配置 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"
注册客户端
接下来,使用之前获得的值进行配置。
以 Okta 为例,配置以下属性:ClientRegistration
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 和客户端密钥的推荐方法。有关详细信息,请参阅 Spring Boot 参考中的外部化配置。OKTA_BASE_URL OKTA_CLIENT_ID OKTA_CLIENT_SECRET |
这个简单示例演示了一个典型的配置,但某些提供程序需要其他配置。
有关配置 的更多信息,请参阅 Spring Security 参考中的 Spring Boot 属性映射。ClientRegistration
配置身份验证
最后,要将 Spring Authorization Server 配置为使用社交登录提供程序进行身份验证,您可以使用代替 .
您还可以通过配置 .oauth2Login()
formLogin()
exceptionHandling()
AuthenticationEntryPoint
继续前面的示例,使用以下示例中的 配置 Spring Security:@Configuration
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 登录以进行身份验证。 |
如果您在开始时配置了 a,则可以立即将其删除。UserDetailsService
上面的例子是 。registrationId my-client |
上面的示例演示了使用环境变量 ( 和 ) 设置提供程序 URL、客户端 ID 和客户端密钥的推荐方法。有关详细信息,请参阅 Spring Boot 参考中的外部化配置。OKTA_BASE_URL OKTA_CLIENT_ID OKTA_CLIENT_SECRET |
1 | 协议端点的 Spring Security 过滤器链。 |
2 | 配置重定向到 OAuth 2.0 登录端点。AuthenticationEntryPoint |
3 | 用于身份验证的 Spring Security 过滤器链。 |
4 | 配置 OAuth 2.0 登录以进行身份验证。 |
高级用例
演示授权服务器示例演示了联合身份提供程序的高级配置选项。 从以下用例中进行选择,查看每个用例的示例:
捕获数据库中的用户
以下示例使用自定义组件在用户首次登录时捕获本地数据库中的用户: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;
}
}
使用上述方法,您可以插入自己的插件,该插件可以在数据库或其他数据存储中捕获用户,以实现联合帐户链接或 JIT 帐户预配等概念。
下面是一个仅将用户存储在内存中的示例:AuthenticationSuccessHandler
Consumer<OAuth2User>
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 令牌
以下示例将用户的声明从身份验证提供程序映射到 Spring 授权服务器生成的声明:OAuth2TokenCustomizer
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);
}
}
您可以通过将 Spring Authorization Server 发布为以下示例中所示来配置此定制器以使用此定制器:@Bean
FederatedIdentityIdTokenCustomizer
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> idTokenCustomizer() {
return new FederatedIdentityIdTokenCustomizer();
}