3. Spring Security OAuth2.0 服务端搭建:授权服务器配置、客户端注册与令牌端点
本章将基于 Spring Security OAuth2 Authorization Server 框架,从零搭建一个生产级授权服务器。核心任务包括:配置授权服务器元数据、注册受信客户端、暴露令牌端点与授权端点,并确保与后续异步导出服务的无缝集成。
版本说明:本课程使用 Spring Authorization Server 1.1.x(基于 Spring Security 6.x),已废弃旧的
@EnableAuthorizationServer 注解,全面采用组件化配置方式。
3.1 授权服务器核心组件与流程
授权服务器在 OAuth2.0 体系中扮演“认证与令牌发放中心”的角色。其核心流程包含三个关键步骤:客户端身份校验、用户授权确认、令牌签发与响应。
3.2 授权服务器配置(核心配置类)
使用 @Configuration + @EnableWebSecurity 注解声明配置类,通过 AuthorizationServerSecurityConfiguration 定制化授权服务器行为。以下为最小化配置模板:
@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // 启用 OpenID Connect 1.0
http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
http.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(
new BearerTokenAuthenticationEntryPoint()));
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}
}
配置要点:
@Order(1)的过滤器链优先处理 OAuth2 请求(路径匹配/oauth2/**)。@Order(2)的默认链处理用户登录(表单登录),用于资源所有者认证。BearerTokenAuthenticationEntryPoint确保令牌失效时返回 401 而非重定向到登录页。
3.3 客户端注册(RegisteredClient)
客户端注册是授权服务器的“白名单”机制。每个调用方(如异步导出服务的前端或后端模块)必须预先注册,获得 client_id 和 client_secret。注册信息存储在 RegisteredClientRepository 中,支持内存、JDBC 或自定义实现。
| 字段 | 说明 | 示例值 |
|---|---|---|
clientId |
客户端唯一标识 | async-export-service |
clientSecret |
客户端密钥(需加密存储) | {noop}secret 或 {bcrypt}$2a$10$... |
authorizationGrantTypes |
允许的授权类型 | AUTHORIZATION_CODE, REFRESH_TOKEN |
redirectUris |
授权码回调地址 | http://localhost:8080/login/oauth2/code/export |
scopes |
允许的权限范围 | export:read, export:write |
tokenSettings |
令牌生命周期与格式 | accessTokenTimeToLive: 30 分钟 |
以下为内存模式客户端注册示例:
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("async-export-service")
.clientSecret("{noop}export-secret-2024")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://localhost:8080/login/oauth2/code/export")
.scope("export:read")
.scope("export:write")
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(30))
.refreshTokenTimeToLive(Duration.ofDays(7))
.build())
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
生产安全提醒:切勿在代码中硬编码
clientSecret。应使用环境变量或密钥管理服务(如 Vault)注入,并采用 {bcrypt} 前缀加密存储。内存模式仅适用于开发/测试环境,生产环境请使用 JdbcRegisteredClientRepository。
3.4 令牌端点与授权端点
Spring Authorization Server 自动注册以下关键端点,无需手动编写 Controller:
| 端点路径 | HTTP 方法 | 功能 | 请求参数 |
|---|---|---|---|
/oauth2/authorize |
GET | 授权端点:用户确认授权后返回授权码 | response_type=code, client_id, redirect_uri, scope, state |
/oauth2/token |
POST | 令牌端点:用授权码或刷新令牌换取访问令牌 | grant_type=authorization_code, code, redirect_uri, client_id, client_secret |
/oauth2/revoke |
POST | 令牌撤销端点 | token, token_type_hint |
/.well-known/oauth-authorization-server |
GET | 授权服务器元数据(OAuth 2.0 Discovery) | 无 |
令牌端点默认返回 JSON 格式响应,包含 access_token、token_type、expires_in、refresh_token 和 scope 字段。以下为成功响应的示例:
{
"access_token": "eyJraWQiOiIx...",
"token_type": "Bearer",
"expires_in": 1799,
"refresh_token": "eyJraWQiOiIy...",
"scope": "export:read export:write"
}
3.5 令牌生成与签名配置
授权服务器使用 JWKSource 生成 RSA 密钥对,对 JWT 令牌进行签名。客户端可通过 /.well-known/oauth-authorization-server 端点获取公钥,用于验证令牌合法性。
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = new RSAKey.Builder(
new RSAPublicKey(...)) // 从 KeyStore 或生成器加载
.privateKey(new RSAPrivateKey(...))
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
性能优化提示:生产环境中建议将
JWKSet 缓存到 Redis,避免每次令牌验证都重新加载密钥对。后续章节将详细讲解 Redis 缓存策略。
3.6 完整配置清单与验证步骤
搭建完成后,可通过以下步骤验证授权服务器是否正常工作:
- 启动服务:访问
http://localhost:9000/.well-known/oauth-authorization-server,确认返回 JSON 元数据。 - 获取授权码:浏览器访问
http://localhost:9000/oauth2/authorize?response_type=code&client_id=async-export-service&redirect_uri=http://localhost:8080/login/oauth2/code/export&scope=export:read,完成表单登录后重定向并携带code参数。 - 换取令牌:使用 curl 或 Postman 向
/oauth2/token发送 POST 请求,携带授权码和客户端凭证,验证返回的access_token是否为 JWT 格式。
curl -X POST http://localhost:9000/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=授权码" \
-d "redirect_uri=http://localhost:8080/login/oauth2/code/export" \
-u "async-export-service:export-secret-2024"
3.7 常见配置陷阱与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 令牌端点返回 401 | 客户端未通过 Basic Auth 或 POST 参数中缺少 client_secret |
确认 ClientAuthenticationMethod.CLIENT_SECRET_BASIC 已配置,且请求头包含 Authorization: Basic base64(client_id:client_secret) |
授权码回调后报 invalid_grant |
授权码已过期(默认 5 分钟)或 redirect_uri 不匹配 |
确保回调 URI 与注册时完全一致(包括端口和路径),并在 5 分钟内完成令牌交换 |
| JWT 令牌无法解码 | 资源服务器未配置正确的 jwk-set-uri 或公钥不匹配 |
在资源服务器配置 spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:9000/.well-known/oauth-authorization-server |
| 刷新令牌失效 | 刷新令牌被重用或超过有效期 | 启用 refreshTokenReuse 为 false(默认),并设置合理的 refreshTokenTimeToLive |