⚠️ 注意:本文内容基于已过时的 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)、email
、name
等。 - 我们使用 授权码模式(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
包含openid
和email
,确保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