1. 概述

本教程将重点介绍如何使用 Spring Security 集成 OpenID Connect (OIDC)。我们将探讨该规范的不同方面,并了解 Spring Security 如何在 OAuth 2.0 客户端中实现这些功能。

2. OpenID Connect 快速入门

OpenID Connect 是构建在 OAuth 2.0 协议之上的身份层。在深入 OIDC 之前,必须先掌握 OAuth 2.0,特别是授权码流程。

OIDC 规范体系非常庞大,包含核心功能和多个可选能力,主要分为以下几部分:

  • 核心:身份认证和通过 Claims 传递终端用户信息
  • 发现:规定客户端如何动态获取 OpenID 提供商信息
  • 动态注册:定义客户端如何向提供商注册
  • 会话管理:管理 OIDC 会话的机制

规范中将支持 OIDC 的 OAuth 2.0 认证服务器称为 **OpenID 提供商 (OP)**,使用 OIDC 的 OAuth 2.0 客户端称为 **依赖方 (RP)**。本文将采用这些术语。

⚠️ 客户端可通过在授权请求中添加 openid scope 来启用此扩展。OP 会将终端用户信息封装在称为 ID Token 的 JWT 中返回。

3. 项目搭建

在开始开发前,需先向 OpenID 提供商注册 OAuth 2.0 客户端。本教程以 Google 作为 OP,可参考官方指南注册应用。注意 openid scope 默认已启用。

注册时设置的回调 URI 为:http://localhost:8081/login/oauth2/code/google。完成后将获得 Client IDClient Secret

3.1. Maven 配置

pom.xml 中添加依赖:

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

该 starter 包含:

  • spring-security-oauth2-client:OAuth 2.0 登录和客户端功能
  • JOSE 库:JWT 支持

✅ 可通过 Maven Central 查找最新版本。

4. 基于 Spring Boot 的基础配置

Spring Boot 让配置变得极其简单,只需定义两个属性:

spring:
  security:
    oauth2:
      client:
        registration: 
          google: 
            client-id: your-google-client-id
            client-secret: your-google-secret

启动应用后访问受保护接口,会自动跳转到 Google 登录页面。看似简单,但底层实现相当复杂。接下来我们分析 Spring Security 如何实现这一过程。

Google 是知名提供商,Spring 已预定义其配置,可在 CommonOAuth2Provider 枚举中查看,包括:

  • 默认 scope
  • 授权接口
  • 令牌接口
  • 用户信息接口(OIDC 核心规范的一部分)

4.1. 访问用户信息

Spring Security 提供 OidcUser 实体表示 OIDC 用户主体,除基础方法外还支持:

  • 获取 ID Token 及其 Claims
  • 获取用户信息接口返回的 Claims
  • 合并两组 Claims

在控制器中可直接获取:

@GetMapping("/oidc-principal")
public OidcUser getOidcUserPrincipal(
  @AuthenticationPrincipal OidcUser principal) {
    return principal;
}

或在 Bean 中通过 SecurityContextHolder 获取:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.getPrincipal() instanceof OidcUser) {
    OidcUser principal = ((OidcUser) authentication.getPrincipal());
    
    // ...
}

检查 principal 会发现用户名、邮箱、头像和地区等实用信息

5. OIDC 实战

目前我们已实现 OIDC 登录,但尚未接触 OIDC 特定细节——Spring 已替我们完成大部分工作。接下来深入分析底层实现,以便充分利用该规范。

5.1. 登录流程

启用 RestTemplate 日志观察请求:

logging:
  level:
    org.springframework.web.client.RestTemplate: DEBUG

访问受保护接口时,会看到标准的 OAuth 2.0 授权码流程,但存在 OIDC 特有差异:

1. 用户信息接口调用
若配置的 scope 包含 profileemailaddressphone,Spring 会调用用户信息接口获取额外数据。但 Google 使用自定义 scope(https://www.googleapis.com/auth/userinfo.emailhttps://www.googleapis.com/auth/userinfo.profile),因此 Spring 不会调用该接口。

所有信息均来自 ID Token。可通过自定义 OidcUserService 适配此行为:

@Configuration
public class OAuth2LoginSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        Set<String> googleScopes = new HashSet<>();
        googleScopes.add("https://www.googleapis.com/auth/userinfo.email");
        googleScopes.add("https://www.googleapis.com/auth/userinfo.profile");

        OidcUserService googleUserService = new OidcUserService();
        googleUserService.setAccessibleScopes(googleScopes);

        http.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
            .oauth2Login(oauthLogin -> oauthLogin.userInfoEndpoint(userInfoEndpointConfig ->
                    userInfoEndpointConfig.oidcUserService(googleUserService)));
        return http.build();
    }
}

2. JWK Set URI 调用
用于验证 ID Token 的签名(参考 JWS 和 JWK)。

5.2. ID Token

在授权码流程中,ID Token 与 Access Token 一起从令牌接口返回。OidcUser 包含 ID Token 的 Claims 和原始 JWT(可通过 jwt.io 检查)。

Spring 提供便捷方法获取标准 Claims,ID Token 必须包含:

  • 发行方标识符(URL 格式,如 https://accounts.google.com
  • 主体标识符(终端用户在 OP 的唯一标识)
  • 过期时间
  • 签发时间
  • 受众(包含配置的客户端 ID)

还包含标准 Claims(如 namelocalepictureemail)。

这些标准字段简化了跨提供商的开发工作

5.3. Claims 与 Scopes

OP 返回的 Claims 取决于请求的 scope。OIDC 定义的标准 scope 包括:

  • profile:请求基础信息(如 namepreferred_usernamepicture
  • email:获取 emailemail_verified
  • address
  • phone:获取 phone_numberphone_number_verified

⚠️ 规范允许在授权请求中指定单个 Claim,但 Spring 尚未支持此功能。

6. Spring 对 OIDC 发现的支持

OIDC 发现机制允许 RP 动态获取 OP 信息。OP 需在 /.well-known/openid-configuration 提供标准元数据 JSON 文档。

Spring 支持通过发行方 URI 自动配置

spring:
  security:
    oauth2:
      client:
        registration: 
          custom-google: 
            client-id: your-google-client-id
            client-secret: your-google-secret
        provider:
          custom-google:
            issuer-uri: https://accounts.google.com

重启应用后,日志会显示启动时调用了 openid-configuration 接口。可直接访问 https://accounts.google.com/.well-known/openid-configuration 查看元数据(如授权接口、令牌接口、支持的 scope 等)。

若发现接口在启动时不可用,应用将无法成功启动

7. OpenID Connect 会话管理

会话管理规范定义了:

  • 监控终端用户在 OP 的登录状态
  • 向 OP 注册 RP 登出 URI
  • 通知 OP 终端用户已登出 RP(可能需同步登出 OP)

本教程重点实现 RP 发起的登出

7.1. OpenID 提供商配置

以 Okta 为例(参考官方指南),Spring 默认回调 URI 为 /login/oauth2/code/okta

应用配置:

spring:
  security:
    oauth2:
      client:
        registration: 
          okta: 
            client-id: your-okta-client-id
            client-secret: your-okta-secret
        provider:
          okta:
            issuer-uri: https://dev-123.okta.com

OP 的登出接口在发现文档的 end_session_endpoint 字段中定义。

7.2. LogoutSuccessHandler 配置

自定义登出逻辑:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(authorizeRequests -> authorizeRequests
                    .requestMatchers("/home").permitAll()
                    .anyRequest().authenticated())
        .oauth2Login(AbstractAuthenticationFilterConfigurer::permitAll)
        .logout(logout -> logout.logoutSuccessHandler(oidcLogoutSuccessHandler()));
    return http.build();
}

使用 Spring 的 OidcClientInitiatedLogoutSuccessHandler

@Autowired
private ClientRegistrationRepository clientRegistrationRepository;

private LogoutSuccessHandler oidcLogoutSuccessHandler() {
    OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
      new OidcClientInitiatedLogoutSuccessHandler(
        this.clientRegistrationRepository);

    oidcLogoutSuccessHandler.setPostLogoutRedirectUri(
      URI.create("http://localhost:8081/home"));

    return oidcLogoutSuccessHandler;
}

⚠️ 需在 OP 客户端配置中将 http://localhost:8081/home 添加为有效的登出重定向 URI。

执行流程

  1. 调用 /logout 接口
  2. 重定向到 OP 的登出接口
  3. 最终跳转至配置的 URI

下次访问受保护接口时,必须重新登录 OP

8. 总结

本文深入探讨了 OpenID Connect 的解决方案及 Spring Security 的实现方式。完整示例代码可在 GitHub 获取。


原始标题:Spring Security and OpenID Connect