Spring Security OAuth2 实战:从授权服务器到微服务网关
介绍 Spring Authorization Server 在 OAuth 2.1 和 OpenID Connect 标准下的应用。涵盖核心概念、授权模式详解、服务端搭建(含数据库存储)、客户端配置、资源服务器集成、单点登录(SSO)架构及微服务网关整合方案。提供版本选择建议、安全最佳实践、性能优化策略及常见问题解决方案,帮助开发者构建安全的认证授权体系。

介绍 Spring Authorization Server 在 OAuth 2.1 和 OpenID Connect 标准下的应用。涵盖核心概念、授权模式详解、服务端搭建(含数据库存储)、客户端配置、资源服务器集成、单点登录(SSO)架构及微服务网关整合方案。提供版本选择建议、安全最佳实践、性能优化策略及常见问题解决方案,帮助开发者构建安全的认证授权体系。

Spring Authorization Server 是 Spring 官方推出的新一代认证授权框架,提供了 OAuth 2.1 和 OpenID Connect 1.0 规范的完整实现。它建立在 Spring Security 之上,为构建身份提供者和授权服务器提供了安全、轻量级且可定制的基础。
核心特性:
官方资源:
随着网络和设备的发展,原有的 OAuth 2.0 协议已无法满足现代应用的安全需求。OAuth 社区推出了 OAuth 2.1 协议,对原有授权模式进行了优化和调整:
Spring Security 团队因此重新开发了 Spring Authorization Server,以替代原有的 Spring Security OAuth 2.0 项目。
四个关键角色:

令牌(Token)与密码(Password)的区别:
适用于:服务端应用间的通信 流程:客户端直接使用 client_id 和 client_secret 获取令牌
请求示例:
POST /oauth2/token grant_type=client_credentials&client_id=CLIENT_ID&client_secret=CLIENT_SECRET
适用于:高度信任的内部应用 流程:用户提供用户名密码,客户端代理获取令牌 注意:OAuth 2.1 中已移除此模式
请求示例:
POST /oauth2/token grant_type=password&username=USERNAME&password=PASSWORD&client_id=CLIENT_ID&client_secret=CLIENT_SECRET
适用于:Web 应用、移动应用 流程:通过授权码中间步骤,安全性最高
流程步骤:
请求示例:
# 1. 获取授权码
GET /oauth2/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=read
# 2. 使用授权码获取令牌
POST /oauth2/token grant_type=authorization_code&code=AUTHORIZATION_CODE&client_id=CLIENT_ID&client_secret=CLIENT_SECRET&redirect_uri=CALLBACK_URL
适用于:单页应用(SPA) 流程:直接返回令牌,跳过授权码步骤 注意:OAuth 2.1 中已移除此模式
适用于:令牌续期 流程:使用 refresh_token 获取新的 access_token
请求示例:
POST /oauth2/token grant_type=refresh_token&client_id=CLIENT_ID&client_secret=CLIENT_SECRET&refresh_token=REFRESH_TOKEN
PKCE(Proof Key for Code Exchange)用于防止授权码被拦截攻击:
工作流程:
适用于智能电视、打印机等输入受限设备:
工作流程:
虽然 OAuth 2.1 移除了密码模式,但可通过拓展授权模式实现类似功能。
OpenID Connect 是建立在 OAuth 2.0 之上的身份层,主要增加了 id_token:
核心特性:
id_token 示例:
{
"iss": "https://server.example.com",
"sub": "24400320",
"aud": "s6BhdRkqt3",
"exp": 1311281970,
"iat": 1311280970,
"auth_time": 1311280969,
"nonce": "n-0S6_WzA2Mj"
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// 授权服务器安全过滤器链
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http
.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // 开启 OpenID Connect
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();
}
// 用户信息服务
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("fox")
.password("123456")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
// 客户端注册信息
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient RegisteredClient.withId(UUID.randomUUID().toString())
.clientId()
.clientSecret()
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri()
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.clientSettings(ClientSettings.builder().requireAuthorizationConsent().build())
.build();
(oidcClient);
}
JWKSource<SecurityContext> {
generateRsaKey();
(RSAPublicKey) keyPair.getPublic();
(RSAPrivateKey) keyPair.getPrivate();
.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
(rsaKey);
<>(jwkSet);
}
KeyPair {
KeyPair keyPair;
{
KeyPairGenerator.getInstance();
keyPairGenerator.initialize();
keyPair = keyPairGenerator.generateKeyPair();
} (Exception ex) {
(ex);
}
keyPair;
}
}
获取授权服务器配置信息:
GET http://127.0.0.1:9000/.well-known/openid-configuration
授权码模式测试:
GET http://localhost:9000/oauth2/authorize?response_type=code&client_id=oidc-client&scope=profile+openid&redirect_uri=http://www.baidu.com
curl -X POST http://localhost:9000/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "oidc-client:secret" \
-d "grant_type=authorization_code" \
-d "code={授权码}" \
-d "redirect_uri=http://www.baidu.com"
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
server:
port: 9001
spring:
application:
name: spring-oauth-client
security:
oauth2:
client:
provider:
oauth-server:
issuer-uri: http://spring-oauth-server:9000
authorization-uri: http://spring-oauth-server:9000/oauth2/authorize
token-uri: http://spring-oauth-server:9000/oauth2/token
registration:
messaging-client-oidc:
provider: oauth-server
client-name: web 平台
client-id: web-client-id
client-secret: secret
client-authentication-method: client_secret_basic
authorization-grant-type: authorization_code
redirect-uri: http://spring-oauth-client:9001/login/oauth2/code/messaging-client-oidc
scope:
- profile
- openid
注意:需要在 hosts 文件中添加域名映射:
127.0.0.1 spring-oauth-client
127.0.0.1 spring-oauth-server
@RestController
public class AuthenticationController {
@GetMapping("/token")
@ResponseBody
public OAuth2AuthorizedClient token(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {
return oAuth2AuthorizedClient;
}
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
server:
port: 9002
spring:
application:
name: spring-oauth-resource
security:
oauth2:
resource-server:
jwt:
issuer-uri: http://spring-oauth-server:9000
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class ResourceServerConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}
@RestController
public class MessagesController {
@GetMapping("/messages1")
public String getMessages1() {
return "hello Message 1";
}
@GetMapping("/messages2")
@PreAuthorize("hasAuthority('SCOPE_profile')")
public String getMessages2() {
return "hello Message 2";
}
@GetMapping("/messages3")
@PreAuthorize("hasAuthority('SCOPE_Message')")
public String getMessages3() {
return "hello Message 3";
}
}
// 认证异常处理
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
if (authException instanceof InvalidBearerTokenException) {
ResponseResult.exceptionResponse(response, "令牌无效或已过期");
} else {
ResponseResult.exceptionResponse(response, "需要带上令牌进行访问");
}
}
}
// 授权异常处理
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
ResponseResult.exceptionResponse(response, "权限不足");
}
}
配置异常处理器:
http.oauth2ResourceServer(resourceServer -> resourceServer
.jwt(Customizer.withDefaults())
.authenticationEntryPoint(new MyAuthenticationEntryPoint())
.accessDeniedHandler(new MyAccessDeniedHandler()));
CREATE TABLE oauth2_registered_client (
id VARCHAR(100) NOT NULL,
client_id VARCHAR(100) NOT NULL,
client_id_issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
client_secret VARCHAR(200) DEFAULT NULL,
client_secret_expires_at TIMESTAMP DEFAULT NULL,
client_name VARCHAR(200) NOT NULL,
client_authentication_methods VARCHAR(1000) NOT NULL,
authorization_grant_types VARCHAR(1000) NOT NULL,
redirect_uris VARCHAR(1000) DEFAULT NULL,
post_logout_redirect_uris VARCHAR(1000) DEFAULT NULL,
scopes VARCHAR(1000) NOT NULL,
client_settings VARCHAR(2000) NOT NULL,
token_settings VARCHAR(2000) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE oauth2_authorization_consent (
registered_client_id VARCHAR(100) NOT NULL,
principal_name VARCHAR(200) NOT NULL,
authorities VARCHAR(1000) NOT NULL,
PRIMARY KEY (registered_client_id, principal_name)
);
CREATE TABLE oauth2_authorization (
id VARCHAR(100) NOT NULL,
registered_client_id VARCHAR(100) NOT NULL,
principal_name VARCHAR(200) NOT NULL,
authorization_grant_type VARCHAR(100) NOT NULL,
authorized_scopes VARCHAR(1000) DEFAULT NULL,
attributes BLOB DEFAULT NULL,
state VARCHAR(500) DEFAULT NULL,
authorization_code_value BLOB DEFAULT NULL,
authorization_code_issued_at TIMESTAMP DEFAULT NULL,
authorization_code_expires_at TIMESTAMP DEFAULT NULL,
authorization_code_metadata BLOB DEFAULT NULL,
access_token_value BLOB DEFAULT NULL,
access_token_issued_at TIMESTAMP DEFAULT NULL,
access_token_expires_at TIMESTAMP DEFAULT NULL,
access_token_metadata BLOB DEFAULT NULL,
access_token_type VARCHAR(100) DEFAULT NULL,
access_token_scopes () ,
oidc_id_token_value ,
oidc_id_token_issued_at ,
oidc_id_token_expires_at ,
oidc_id_token_metadata ,
refresh_token_value ,
refresh_token_issued_at ,
refresh_token_expires_at ,
refresh_token_metadata ,
user_code_value ,
user_code_issued_at ,
user_code_expires_at ,
user_code_metadata ,
device_code_value ,
device_code_issued_at ,
device_code_expires_at ,
device_code_metadata ,
(id)
);
CREATE TABLE sys_user (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'id',
username VARCHAR(20) NOT NULL DEFAULT '' COMMENT '用户名',
password VARCHAR(255) NOT NULL DEFAULT '' COMMENT '密码',
name VARCHAR(50) DEFAULT NULL COMMENT '姓名',
description VARCHAR(255) DEFAULT NULL COMMENT '描述',
status TINYINT DEFAULT NULL COMMENT '状态(1:正常 0:停用)',
PRIMARY KEY (id),
UNIQUE KEY idx_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
spring:
application:
name: spring-oauth-server
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/oauth-server?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
username: root
password: root
@Configuration
public class DatabaseConfig {
// 客户端信息存储
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
return new JdbcRegisteredClientRepository(jdbcTemplate);
}
// 授权信息存储
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
// 授权确认存储
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
// 密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUserEntity sysUserEntity = sysUserService.selectByUsername(username);
List<SimpleGrantedAuthority> authorities = Arrays.asList("USER").stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return new User(username, sysUserEntity.getPassword(), authorities);
}
}
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 订单服务 │ │ 商品服务 │ │ 认证服务器 │
│ (客户端) │ │ (客户端) │ │ (授权服务器)│
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└───────────────┼───────────────┘
│
┌──────┴──────┐
│ 用户 │
│ (浏览器) │
└─────────────┘
server:
ip: spring-oauth-client-order
port: 9003
spring:
application:
name: spring-oauth-client-order
security:
oauth2:
client:
provider:
oauth-server:
issuer-uri: http://spring-oauth-server:9000
authorization-uri: http://spring-oauth-server:9000/oauth2/authorize
token-uri: http://spring-oauth-server:9000/oauth2/token
registration:
messaging-client-oidc:
provider: oauth-server
client-name: web 平台-SSO 客户端 - 订单服务
client-id: web-client-id-order
client-secret: secret
client-authentication-method: client_secret_basic
authorization-grant-type: authorization_code
redirect-uri: http://spring-oauth-client-order:9003/login/oauth2/code/messaging-client-oidc
scope:
- profile
- openid
server:
ip: spring-oauth-client-product
port: 9004
spring:
application:
name: spring-oauth-client-product
security:
oauth2:
client:
provider:
oauth-server:
issuer-uri: http://spring-oauth-server:9000
authorization-uri: http://spring-oauth-server:9000/oauth2/authorize
token-uri: http://spring-oauth-server:9000/oauth2/token
registration:
messaging-client-oidc:
provider: oauth-server
client-name: web 平台-SSO 客户端 - 商品服务
client-id: web-client-id-product
client-secret: secret
client-authentication-method: client_secret_basic
authorization-grant-type: authorization_code
redirect-uri: http://spring-oauth-client-product:9004/login/oauth2/code/messaging-client-oidc
scope:
- profile
- openid
require-authorization-consent=false 跳过授权确认┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 用户 │───▶│ 网关 │───▶│ 认证服务器│
│ (浏览器) │ │ (Gateway) │ │ (OAuth2) │
└─────────────┘ └──────┬──────┘ └─────────────┘
│
┌──────┴──────┐
│ 资源服务 │
│ (微服务) │
└─────────────┘
<!-- OAuth2 客户端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>3.1.4</version>
</dependency>
<!-- OAuth2 资源服务器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>3.1.4</version>
</dependency>
server:
port: 8888
spring:
application:
name: mall-gateway
security:
oauth2:
# 资源服务器配置
resourceserver:
jwt:
issuer-uri: http://spring-oauth-server:9000
# 客户端配置
client:
provider:
oauth-server:
issuer-uri: http://spring-oauth-server:9000
authorization-uri: http://spring-oauth-server:9000/oauth2/authorize
token-uri: http://spring-oauth-server:9000/oauth2/token
registration:
messaging-client-oidc:
provider: oauth-server
client-name: 网关服务
client-id: mall-gateway-id
client-secret: secret
client-authentication-method: client_secret_basic
authorization-grant-type: authorization_code
redirect-uri: http://mall-gateway:8888/login/oauth2/code/messaging-client-oidc
scope:
- profile
- openid
cloud:
gateway:
default-filters:
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class WebSecurityConfig {
@Bean
public SecurityWebFilterChain defaultSecurityFilterChain(ServerHttpSecurity http) {
// 所有请求都需要认证
http.authorizeExchange(authorize -> authorize.anyExchange().authenticated());
// 开启 OAuth2 登录
http.oauth2Login(Customizer.withDefaults());
// 配置资源服务器
http.oauth2ResourceServer(resourceServer -> resourceServer.jwt(Customizer.withDefaults()));
// 禁用 CSRF 和 CORS
http.csrf(csrf -> csrf.disable());
http.cors(cors -> cors.disable());
return http.build();
}
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>3.1.4</version>
</dependency>
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://spring-oauth-server:9000
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class ResourceServerConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}
@Slf4j
@Component
public class FeignAuthRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String accessToken = request.getHeader("Authorization");
log.info("从 Request 中解析请求头:{}", accessToken);
// 设置 token 到 Feign 请求头
template.header("Authorization", accessToken);
}
}
}
http://mall-gateway:8888/user/findOrderByUserId/1| 组件 | 推荐版本 | 说明 |
|---|---|---|
| Spring Boot | 3.1.4+ | 支持 Spring Authorization Server 最新特性 |
| Spring Authorization Server | 1.1.2+ | 稳定版本,功能完整 |
| JDK | 17+ | Spring Boot 3.x 要求 |
| MySQL | 8.0+ | 支持 JSON 字段,性能更好 |
解决方案:
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
配置分布式会话:
spring:
session:
store-type: redis
redis:
namespace: spring:session
自动刷新令牌策略:
@Component
public class TokenRefreshService {
@Scheduled(fixedDelay = 5 * 60 * 1000) // 每 5 分钟检查一次
public void refreshTokens() {
// 检查即将过期的 token 并刷新
}
}
参考资料:
本文基于 Spring Authorization Server 1.1.2 和 Spring Boot 3.1.4 编写,涵盖了从基础概念到生产级部署的全流程。在实际项目中,建议根据具体业务需求和安全要求进行调整和优化。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online