⚠️ 注意:本文内容基于已过时的 Spring Security OAuth 旧技术栈。建议查阅 Spring Security 最新的 OAuth 支持 以获取现代实践方式。

本文适用于需要维护旧项目或理解历史实现的开发者,不推荐用于新项目。


1. 概述

本文带你快速搭建基于 Spring Security OAuth2 的 OpenID Connect 认证功能。

OpenID Connect 是构建在 OAuth 2.0 协议之上的身份认证层,用于验证用户身份。简单来说,它在 OAuth2 获取授权的基础上,额外返回一个 id_token,里面包含了用户的身份信息。

我们将以 Google 的 OpenID Connect 实现 为例,演示如何完成用户登录认证。

✅ 你得先对 OAuth2 的授权码模式(Authorization Code Flow)有基本理解,否则看这篇会踩坑。


2. Maven 依赖配置

在 Spring Boot 项目中引入以下核心依赖:

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

⚠️ 注意:spring-security-oauth2 是旧版模块,Spring 官方已不再积极维护,新项目应使用 spring-security-oauth2-client


3. 理解 Id Token

在动手编码前,先搞清楚 id_token 是什么。

  • 添加 openid 作用域(scope)后,OAuth2 流程会返回一个 JWT 格式的 id_token
  • 这个 token 由身份提供商(如 Google)签名,包含用户身份信息,比如 sub(唯一用户 ID)、emailname 等。
  • 我们使用 授权码模式(Server Flow) 来获取 id_token,这是最安全的流程。

4. OAuth2 客户端配置

配置 Google 作为 OAuth2 客户端,关键是要设置好客户端凭证和端点(endpoint)。

4.1 Java 配置类

@Configuration
@EnableOAuth2Client
public class GoogleOpenIdConnectConfig {

    @Value("${google.clientId}")
    private String clientId;

    @Value("${google.clientSecret}")
    private String clientSecret;

    @Value("${google.accessTokenUri}")
    private String accessTokenUri;

    @Value("${google.userAuthorizationUri}")
    private String userAuthorizationUri;

    @Value("${google.redirectUri}")
    private String redirectUri;

    @Bean
    public OAuth2ProtectedResourceDetails googleOpenId() {
        AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
        details.setClientId(clientId);
        details.setClientSecret(clientSecret);
        details.setAccessTokenUri(accessTokenUri);
        details.setUserAuthorizationUri(userAuthorizationUri);
        details.setScope(Arrays.asList("openid", "email"));
        details.setPreEstablishedRedirectUri(redirectUri);
        details.setUseCurrentUri(false);
        return details;
    }

    @Bean
    public OAuth2RestTemplate googleOpenIdTemplate(OAuth2ClientContext clientContext) {
        return new OAuth2RestTemplate(googleOpenId(), clientContext);
    }
}

4.2 application.properties

google.clientId=your-google-client-id-12345.apps.googleusercontent.com
google.clientSecret=your-google-client-secret
google.accessTokenUri=https://www.googleapis.com/oauth2/v3/token
google.userAuthorizationUri=https://accounts.google.com/o/oauth2/auth
google.redirectUri=http://localhost:8081/google-login

✅ 注意事项:

  • 客户端 ID 和密钥需在 Google Cloud Console 创建 OAuth2 凭据后获取。
  • scope 包含 openidemail,确保 id_token 中包含邮箱信息。
  • redirectUri 必须与 Google 控制台中注册的回调地址完全一致。

5. 自定义 OpenID Connect 过滤器

Spring Security 旧版不直接支持 OpenID Connect,所以我们得自己写一个过滤器来解析 id_token

5.1 OpenIdConnectFilter 实现

public class OpenIdConnectFilter extends AbstractAuthenticationProcessingFilter {

    @Autowired
    private OAuth2RestTemplate restTemplate;

    public OpenIdConnectFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
        setAuthenticationManager(new NoopAuthenticationManager());
    }

    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {

        OAuth2AccessToken accessToken;
        try {
            accessToken = restTemplate.getAccessToken();
        } catch (OAuth2Exception e) {
            throw new BadCredentialsException("Could not obtain access token", e);
        }

        try {
            String idToken = accessToken.getAdditionalInformation().get("id_token").toString();
            String kid = JwtHelper.headers(idToken).get("kid");
            Jwt tokenDecoded = JwtHelper.decodeAndVerify(idToken, verifier(kid));
            Map<String, String> authInfo = new ObjectMapper()
                    .readValue(tokenDecoded.getClaims(), Map.class);
            verifyClaims(authInfo);
            OpenIdConnectUserDetails user = new OpenIdConnectUserDetails(authInfo, accessToken);
            return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
        } catch (InvalidTokenException e) {
            throw new BadCredentialsException("Could not obtain user details from token", e);
        }
    }

    // getter/setter for restTemplate
    public void setRestTemplate(OAuth2RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
}

5.2 OpenIdConnectUserDetails

public class OpenIdConnectUserDetails implements UserDetails {
    private String userId;
    private String username;
    private OAuth2AccessToken token;

    public OpenIdConnectUserDetails(Map<String, String> userInfo, OAuth2AccessToken token) {
        this.userId = userInfo.get("sub");
        this.username = userInfo.get("email");
        this.token = token;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.emptyList();
    }

    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

6. Token 验证:签名与 Claims

光解析 token 不够,必须验证其完整性和有效性,否则有安全风险。

6.1 引入 JWKS 支持库

Google 使用 JWKS(JSON Web Key Set)动态管理公钥,我们需要用库来获取:

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>jwks-rsa</artifactId>
    <version>0.3.0</version>
</dependency>

6.2 配置 JWKS 地址

google.jwkUrl=https://www.googleapis.com/oauth2/v2/certs

6.3 构建 RSAVerifier

@Value("${google.jwkUrl}")
private String jwkUrl;

private RsaVerifier verifier(String kid) throws Exception {
    JwkProvider provider = new UrlJwkProvider(new URL(jwkUrl));
    Jwk jwk = provider.get(kid);
    return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}

6.4 验证 Claims

private static final String ISSUER_GOOGLE = "https://accounts.google.com";
private String clientId = "your-google-client-id-12345.apps.googleusercontent.com";

public void verifyClaims(Map<String, Object> claims) {
    Integer exp = (Integer) claims.get("exp");
    Date expireDate = new Date(exp * 1000L);
    Date now = new Date();

    String iss = (String) claims.get("iss");
    String aud = (String) claims.get("aud");

    if (expireDate.before(now) ||
        !ISSUER_GOOGLE.equals(iss) ||
        !clientId.equals(aud)) {
        throw new RuntimeException("Invalid claims: token expired or invalid issuer/audience");
    }
}

✅ 验证项包括:

  • ✅ 签名是否由 Google 的公钥签发(通过 kid 匹配)
  • exp 是否过期
  • iss(签发者)是否为 Google
  • aud(受众)是否为当前客户端 ID

7. 安全配置

将自定义过滤器接入 Spring Security 过滤链。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private OAuth2RestTemplate restTemplate;

    @Bean
    public OpenIdConnectFilter openIdConnectFilter() {
        OpenIdConnectFilter filter = new OpenIdConnectFilter("/google-login");
        filter.setRestTemplate(restTemplate);
        return filter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .addFilterAfter(new OAuth2ClientContextFilter(),
                AbstractPreAuthenticatedProcessingFilter.class)
            .addFilterAfter(openIdConnectFilter(),
                OAuth2ClientContextFilter.class)
            .httpBasic().disable()
            .authorizeHttpRequests(authz -> authz
                .anyRequest().authenticated()
            )
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/google-login"))
            );
        return http.build();
    }
}

⚠️ 关键点:

  • OAuth2ClientContextFilter 必须在前,用于维护 OAuth2 上下文。
  • OpenIdConnectFilter 跟在其后,处理 /google-login 回调。
  • 未认证请求会被重定向到 /google-login,触发 Google 登录流程。

8. 测试控制器

写个简单接口验证登录是否成功。

@Controller
public class HomeController {

    @RequestMapping("/")
    @ResponseBody
    public String home() {
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        return "Welcome, " + username;
    }
}

登录成功后访问 /,输出示例:

Welcome, user@example.com

9. OpenID Connect 流程详解

下面是整个认证流程的简化版,帮助你理解底层发生了什么。

9.1 第一步:发起认证请求

浏览器跳转至 Google 登录页:

GET https://accounts.google.com/o/oauth2/auth?
  client_id=your-client-id&
  response_type=code&
  scope=openid%20email&
  redirect_uri=http://localhost:8081/google-login&
  state=abc123

9.2 第二步:用户授权后回调

用户同意后,Google 重定向回你的应用:

HTTP/1.1 302 Found
Location: http://localhost:8081/google-login?code=auth-code-xyz&state=abc123

9.3 第三步:用 code 换取 token

后端用 code 向 Google Token 接口发起 POST 请求:

POST https://www.googleapis.com/oauth2/v3/token
Content-Type: application/x-www-form-urlencoded

code=auth-code-xyz&
client_id=your-client-id&
client_secret=your-client-secret&
redirect_uri=http://localhost:8081/google-login&
grant_type=authorization_code

9.4 第四步:Google 返回 token

{
    "access_token": "ya29.a0AfB_byCHabcdefgh...",
    "id_token": "eyJhbGciOiJSUzI1NiIs...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "1//03xyz..."
}

9.5 第五步:解析 id_token 内容

解码 id_token 后的 payload 示例:

{
    "iss": "https://accounts.google.com",
    "at_hash": "abc123",
    "sub": "112233445566778899",
    "email": "user@example.com",
    "email_verified": true,
    "name": "John Doe",
    "picture": "https://lh3.googleusercontent.com/...",
    "locale": "en",
    "exp": 1735689600,
    "iat": 1735686000
}

✅ 你可以直接从 id_token 拿到用户信息,无需再调用 /userinfo 接口,简单粗暴。


10. 总结

本文演示了如何在 Spring Security 旧版 OAuth2 框架下集成 Google OpenID Connect,核心是:

  • ✅ 使用 openid scope 获取 id_token
  • ✅ 自定义 OpenIdConnectFilter 解析并验证 JWT
  • ✅ 验证签名(JWKS)和 Claims(exp, iss, aud)
  • ✅ 将用户信息注入 Spring Security 上下文

⚠️ 再次提醒:该方案基于已淘汰的技术栈。新项目请使用 Spring Security 5+ 的 spring-security-oauth2-client 模块,原生支持 OIDC,配置更简洁,安全性更高。

代码示例可在 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/spring-security-modules/spring-security-legacy-oidc


原始标题:Spring Security and OpenID Connect (Legacy)