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的授权服务实体模型

首先需要定义实体来表示核心组件:RegisteredClientOAuth2AuthorizationOAuth2AuthorizationConsent。我们根据授权类型拆分了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):通过idclientId查找OAuth2RegisteredClient
  • 授权同意仓库 (OAuth2UserConsentRepository):通过registeredClientIdprincipalName查找/删除OAuth2UserConsent记录
  • 授权授权仓库 (OAuth2AuthorizationGrantAuthorizationRepository):根据授权类型通过idstatedeviceCode等查找OAuth2AuthorizationGrantAuthorization

仓库实现代码可在GitHub查看。

3.3 基于Redis的授权服务核心服务

然后构建对应的核心服务

  • 模型映射器 (ModelMapper):非核心服务,但用于在所有核心服务中转换实体对象和领域对象
  • 注册客户端仓库 (RedisRegisteredClientRepository):组合OAuth2RegisteredClientRepositoryOAuth2RegisteredClientModelMapper持久化RegisteredClient对象
  • 授权同意服务 (RedisOAuth2AuthorizationConsentService):组合OAuth2UserConsentRepositoryOAuth2UserConsentModelMapper持久化OAuth2AuthorizationConsent对象
  • 授权服务 (RedisOAuth2AuthorizationService):组合OAuth2AuthorizationGrantAuthorizationRepositoryOAuth2AuthorizationGrantAuthorizationModelMapper持久化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):

Spring OAuth with Redis - 三个服务运行界面

访问http://127.0.0.1:8080/articles,页面将重定向到登录页:

Spring OAuth with Redis - 登录界面

用户可选择通过articles-client-authorization-codearticles-client-oidc认证。两种方式都需要输入用户名和密码("admin"/"password",在UserDetailsService Bean中设置)。

认证成功后,用户获得访问articles-client的权限。若选择第一种方式,登录后需再次点击第二个链接才能访问文章页;若选择第二种方式,则自动跳转:

Spring OAuth with Redis - 登录后的文章页

图中可见登录成功后用户可访问资源,且浏览器已添加Cookie。

⚠️ 关键点:由于我们使用Redis实现了核心服务,现在Redis服务器会存储会话等数据:

Spring OAuth with Redis - Redis存储数据

登录前Redis中只有oauth2_registered_client对象,其余均为登录后存储的数据。

5. 结论

本文探讨了使用Redis实现Spring授权服务器核心服务的方案。我们分析了从默认内存存储切换到Redis所需修改的组件,并演示了认证用户获取资源访问权限的流程,以及授权服务器在Redis中存储令牌的过程。

所有示例源代码可在GitHub获取。