1. 概述
Spring Authorization Server的默认实现将所有数据存储在内存中。包括RegisteredClient、令牌存储、授权状态等,每次JVM启动/停止时都会重新创建和删除。这在演示和测试等场景下很方便,但在实际生产环境中会带来问题——无法支持水平扩展、服务重启等场景。
为解决这一问题,Spring提供了使用Redis实现授权服务器核心服务的方案。这样我们可以实现令牌和注册客户端的持久化存储,同时获得更好的质量和安全性,包括令牌管理能力。我们还能扩展授权服务器、提供可观测性和事件溯源,并支持跨节点撤销令牌等特性。
本文将探讨如何使用Redis实现Spring授权服务器的核心服务。我们会分析需要修改或添加的组件,并提供实现代码示例。演示中我们使用嵌入式Redis服务器,但所有方案同样适用于容器化或独立部署的Redis实例。
2. 基础项目
本教程基于现有的Spring Security OAuth项目进行改造,该项目默认使用内存存储。我们将在此基础上引入Redis实现的核心服务。
基础项目是一个提供文章列表的REST API,但接口需要认证和授权(如链接文章所述)。在本次教程范围内,我们不会对服务本身做任何修改。
2.1 项目结构
基础项目包含三个模块:
- 授权服务器:作为文章资源和客户端服务器的认证源
- 资源服务器:在验证授权后提供文章列表
- 客户端服务器:REST API客户端,在用户认证后获取文章
本文将保持基础项目代码不变,仅对授权服务器进行改造。
2.2 依赖项
首先定义所有模块共用的Spring依赖版本。所有模块都使用一个公共父POM,该父POM又继承自spring-boot-starter-parent:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
</parent>
另一个需要声明的依赖是嵌入式Redis服务器(用于授权服务器):
<dependency>
<groupId>com.github.codemonstur</groupId>
<artifactId>embedded-redis</artifactId>
<version>1.4.2</version>
</dependency>
3. 基于Redis的Spring授权服务器
Spring Authorization Server默认对以下组件使用内存实现:
- RegisteredClientRepository
- 令牌存储
- 授权同意
- 授权状态
这在需要快速启动且无需持久化的场景(如测试、演示)中很有用。但当用例更复杂,需要长期存储、扩展能力和监控时,授权服务器必须支持数据库存储。
Spring提供了使用Redis实现核心服务的方案。实现步骤包括:
- 定义实体模型
- 创建Spring Data仓库
- 实现核心服务
- 配置核心服务
3.1 基于Redis的授权服务实体模型
首先需要定义实体来表示核心组件:RegisteredClient、OAuth2Authorization和OAuth2AuthorizationConsent。我们根据授权类型拆分了OAuth2Authorization类。需要创建的实体包括:
- 注册客户端实体 (OAuth2RegisteredClient):持久化RegisteredClient映射信息
- 授权同意实体 (OAuth2UserConsent):持久化OAuth2AuthorizationConsent映射信息
- 授权授权基础实体 (OAuth2AuthorizationGrantAuthorization):持久化OAuth2Authorization的基类,包含各授权类型的公共属性
- OAuth 2.0授权码授权实体 (OAuth2AuthorizationCodeGrantAuthorization):定义OAuth 2.0"authorization_code"类型的附加属性
- OpenID Connect 1.0授权码授权实体 (OidcAuthorizationCodeGrantAuthorization):定义OpenID Connect 1.0"authorization_code"类型的附加属性
- 客户端凭据授权实体 (OAuth2ClientCredentialsGrantAuthorization):定义"client_credentials"类型的附加属性
- 设备码授权实体 (OAuth2DeviceCodeGrantAuthorization):定义"urn:ietf:params:oauth:grant-type:device_code"类型的附加属性
- 令牌交换授权实体 (OAuth2TokenExchangeGrantAuthorization):定义"urn:ietf:params:oauth:grant-type:token-exchange"类型的附加属性
实体实现代码可在GitHub查看。
3.2 基于Redis的授权服务Spring Data仓库
接着创建实现核心服务所需的最小仓库集合:
- 注册客户端仓库 (OAuth2RegisteredClientRepository):通过id或clientId查找OAuth2RegisteredClient
- 授权同意仓库 (OAuth2UserConsentRepository):通过registeredClientId和principalName查找/删除OAuth2UserConsent记录
- 授权授权仓库 (OAuth2AuthorizationGrantAuthorizationRepository):根据授权类型通过id、state、deviceCode等查找OAuth2AuthorizationGrantAuthorization
仓库实现代码可在GitHub查看。
3.3 基于Redis的授权服务核心服务
然后构建对应的核心服务:
- 模型映射器 (ModelMapper):非核心服务,但用于在所有核心服务中转换实体对象和领域对象
- 注册客户端仓库 (RedisRegisteredClientRepository):组合OAuth2RegisteredClientRepository、OAuth2RegisteredClient和ModelMapper持久化RegisteredClient对象
- 授权同意服务 (RedisOAuth2AuthorizationConsentService):组合OAuth2UserConsentRepository、OAuth2UserConsent和ModelMapper持久化OAuth2AuthorizationConsent对象
- 授权服务 (RedisOAuth2AuthorizationService):组合OAuth2AuthorizationGrantAuthorizationRepository、OAuth2AuthorizationGrantAuthorization和ModelMapper持久化OAuth2Authorization对象
服务实现代码可在GitHub查看。
3.4 基于Redis的授权服务Spring配置
最后创建启用Redis核心服务的Spring配置文件。
SecurityConfig类应包含与基础项目相同的Bean。演示中使用的用户凭据为用户名"admin",密码"password"。Redis配置如下:
@Configuration(proxyBeanMethods = false)
@EnableRedisRepositories
public class RedisConfig {
// 字段省略
@PostConstruct
public void postConstruct() throws IOException {
redisServer.start();
}
@PreDestroy
public void preDestroy() throws IOException {
redisServer.stop();
}
@Bean
@Order(1)
public JedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration
= new RedisStandaloneConfiguration(redisHost, redisPort);
return new JedisConnectionFactory(redisStandaloneConfiguration);
}
@Bean
@Order(2)
public RedisTemplate<?, ?> redisTemplate(JedisConnectionFactory connectionFactory) {
RedisTemplate<byte[], byte[]> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
return template;
}
@Bean
@Order(3)
public RedisCustomConversions redisCustomConversions() {
return new RedisCustomConversions(
Arrays.asList(
new UsernamePasswordAuthenticationTokenToBytesConverter(),
new BytesToUsernamePasswordAuthenticationTokenConverter(),
new OAuth2AuthorizationRequestToBytesConverter(),
new BytesToOAuth2AuthorizationRequestConverter(),
new ClaimsHolderToBytesConverter(),
new BytesToClaimsHolderConverter()));
}
@Bean
@Order(4)
public RedisRegisteredClientRepository registeredClientRepository(
OAuth2RegisteredClientRepository registeredClientRepository) {
RedisRegisteredClientRepository redisRegisteredClientRepository
= new RedisRegisteredClientRepository(registeredClientRepository);
redisRegisteredClientRepository.save(RegisteredClients.messagingClient());
return redisRegisteredClientRepository;
}
@Bean
@Order(5)
public RedisOAuth2AuthorizationService authorizationService(
RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationGrantAuthorizationRepository authorizationGrantAuthorizationRepository) {
return new RedisOAuth2AuthorizationService(registeredClientRepository,
authorizationGrantAuthorizationRepository);
}
@Bean
@Order(6)
public RedisOAuth2AuthorizationConsentService authorizationConsentService(
OAuth2UserConsentRepository userConsentRepository) {
return new RedisOAuth2AuthorizationConsentService(userConsentRepository);
}
}
RedisConfig类提供了Redis和授权服务器核心组件的Spring Bean:
✅ @PostConstruct和*@PreDestroy用于启动/停止嵌入式Redis服务器
✅ JedisConnectionFactory和RedisTemplate用于连接Redis
✅ RedisCustomConversions提供Redis持久化所需的对象到哈希转换
✅ RedisRegisteredClientRepository设置registeredClientRepository* Bean并注册客户端(下一节详述)
✅ RedisOAuth2AuthorizationService注册为authorizationService Bean
✅ RedisOAuth2AuthorizationConsentService注册为authorizationConsentService Bean
3.5 注册客户端
使用默认内存存储时,可通过属性定义注册客户端。这是Spring Authorization Server 3.1.0版本起的内置特性,通过OAuth2AuthorizationServerProperties类实现。
但在我们的实现中,由于使用了自定义的RegisteredClientRepository,该特性不再受支持。
可通过多种方式解决,如使用OAuth2AuthorizationServerProperties类并自行映射存储到自定义仓库,或直接使用硬编码值等。本教程采用最简单的硬编码方式,直接在配置中将RegisteredClient存入RedisRegisteredClientRepository(如上节所示)。
使用代码创建RegisteredClient:
public class RegisteredClients {
public static RegisteredClient messagingClient() {
return RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("articles-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc")
.redirectUri("http://127.0.0.1:8080/authorized")
.postLogoutRedirectUri("http://127.0.0.1:9000/login")
.scope(OidcScopes.OPENID)
.scope("articles.read")
.build();
}
}
该配置与基础项目中的属性定义客户端相同。注意我们使用了所有四种已创建实体对应的授权类型。
4. 基于Redis的授权服务演示
现在演示如何获取articles资源。首先启动三个模块(本例使用IntelliJ):
访问http://127.0.0.1:8080/articles,页面将重定向到登录页:
用户可选择通过articles-client-authorization-code或articles-client-oidc认证。两种方式都需要输入用户名和密码("admin"/"password",在UserDetailsService Bean中设置)。
认证成功后,用户获得访问articles-client的权限。若选择第一种方式,登录后需再次点击第二个链接才能访问文章页;若选择第二种方式,则自动跳转:
图中可见登录成功后用户可访问资源,且浏览器已添加Cookie。
⚠️ 关键点:由于我们使用Redis实现了核心服务,现在Redis服务器会存储会话等数据:
登录前Redis中只有oauth2_registered_client对象,其余均为登录后存储的数据。
5. 结论
本文探讨了使用Redis实现Spring授权服务器核心服务的方案。我们分析了从默认内存存储切换到Redis所需修改的组件,并演示了认证用户获取资源访问权限的流程,以及授权服务器在Redis中存储令牌的过程。
所有示例源代码可在GitHub获取。