知識庫 / Spring / Spring Security RSS 訂閱

使用Redis實現Spring Authorization Server的核心服務

Spring Security
HongKong
9
10:45 AM · Dec 06 ,2025

1. 概述

Spring Authorization Server 的默認實現將所有內容存儲在內存中。例如,註冊客户端、令牌存儲、授權狀態等,都會在 JVM 啓動/停止時創建和刪除。這在某些情況下,如演示和測試,是有益的。但是,在實際應用中,它 會導致問題,因為它不支持水平擴展、重啓和其他類似場景

為了解決這個問題,Spring 提供方法,用於使用 Redis 實現 Spring Authorization Service 的核心服務。 這樣,我們就可以擁有令牌和註冊客户端的持久性和耐久性。此外,我們還可以享受更好的質量和安全性,並可以訪問管理令牌。我們可以擴展 Authorization Server,提供可觀測性和事件溯源,並可以在多個節點上訪問撤銷令牌,以及其他好處。

在本文中,我們將探討如何使用 Redis 實現 Spring Authorization Service 的核心服務。我們將研究需要更改或添加的組件,並提供代碼示例以實現這些功能。我們將使用嵌入式 Redis 服務器進行演示目的。但是,所有內容都應該在容器化或部署實例中以相同的方式工作。

2. 基礎項目

為了本教程,我們將基於現有的 Spring Security OAuth 項目進行演示代碼的構建,該項目將所有數據存儲在內存中。然後,我們將介紹 Spring Authorization Service 的核心服務以及 Redis 的使用。

現有的項目是一個 REST API,它提供了一份文章列表。但是,該端點已進行安全保護,需要身份驗證和授權,如鏈接文章中所述。在本教程的範圍內,我們不會對服務本身進行任何修改。

2.1. 項目結構

本項目的基線包含三個模塊:

  • 身份驗證服務器作為文章資源和客户端服務器的身份驗證來源。
  • 資源服務器在驗證身份驗證與身份驗證服務器的有效性後,提供文章列表。
  • 客户端服務器是一個 REST API 客户端,在用户身份驗證後,向資源服務器發起授權請求,並獲取文章。

在本文中,我們將使用與基線項目相同的代碼,並對身份驗證服務器進行一些修改。

2.2. 依賴關係

首先,我們定義 Spring 依賴關係中的常用版本,以便在所有模塊中使用。為此,所有模塊都將使用一個通用的父模塊,該父模塊反過來使用 spring-boot-starter-parent 作為其父模塊:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.0</version>
</parent>

我們需要提及的另一個版本是嵌入式 Redis Server 依賴,用於在授權服務器中使用:

<dependency>
    <groupId>com.github.codemonstur</groupId>
    <artifactId>embedded-redis</artifactId>
    <version>1.4.2</version>
</dependency>

3. 使用 Redis 的 Spring Authorization Server

Spring Authorization Server 默認情況下,對於以下內容使用內存實現:

  • RegisteredClientRepository
  • Token 存儲
  • 授權同意
  • 授權狀態

這在某些場景中很有益,尤其當我們無需長期存儲即可快速行動時。這些場景可能包括運行測試、演示目的以及其他情況。但是,如果用例更復雜,需要長期存儲、可擴展性以及監控要求,則 Authorization Server 需要能夠將內容存儲在數據庫中。

Spring 提供此選項,可以使用 Redis 實現 Spring Authorization Service 的核心服務。要做到這一點,我們需要:

  • 定義實體模型
  • 創建 Spring Data 存儲庫
  • 實現核心服務
  • 配置核心服務

3.1. 授權服務中的實體模型(帶 Redis)

首先,我們需要定義一些實體來表示核心組件:RegisteredClientOAuth2AuthorizationOAuth2AuthorizationConsent。 我們根據授權請求類型將 OAuth2Authorization 類分解了。

  1. RegisteredClient 實體(OAuth2RegisteredClient)用於持久化從 RegisteredClient 映射的信息。
  2. Authorization Consent 實體(OAuth2UserConsent)用於持久化從 OAuth2AuthorizationConsent 映射的信息。
  3. Authorization Grant 基礎實體(OAuth2AuthorizationGrantAuthorization)是用於持久化映射到 OAuth2Authorization 的基礎實體,並具有每個授權請求類型的通用屬性。
  4. OAuth 2.0 中 Authorization Code 實體(OAuth2AuthorizationCodeGrantAuthorization)定義了 OAuth 2.0 “authorization_code” 授權請求類型的附加屬性。
  5. OpenID Connect 1.0 中 Authorization Code 實體(OidcAuthorizationCodeGrantAuthorization)定義了 OpenID Connect 1.0 “authorization_code” 授權請求類型的附加屬性。
  6. Client Credentials 實體(OAuth2ClientCredentialsGrantAuthorization)定義了 “client_credentials” 授權請求類型的附加屬性。
  7. Device Code 實體(OAuth2DeviceCodeGrantAuthorization)定義了 “urn:ietf:params:oauth:grant-type:device_code” 授權請求類型的附加屬性。
  8. Token Exchange 實體(OAuth2TokenExchangeGrantAuthorization)定義了 “urn:ietf:params:oauth:grant-type:token-exchange” 授權請求類型的附加屬性。

3.2. 使用 Redis 實現的授權服務倉庫

然後,我們創建了為了實現 Spring 授權服務與 Redis 的核心服務所需的最小倉庫集。這些倉庫如下:

  • OAuth2RegisteredClientRepository (註冊客户端倉庫) 用於通過 idclientId 查找 OAuth2RegisteredClient
  • OAuth2UserConsentRepository (授權同意倉庫) 用於通過 registeredClientIdprincipalName 查找和刪除 OAuth2UserConsent 記錄
  • OAuth2AuthorizationGrantAuthorizationRepository (授權授權碼倉庫) 用於通過 idstatedeviceCode 等,根據授權碼類型查找 OAuth2AuthorizationGrantAuthorization

3.3. 授權服務的核心服務與 Redis

我們同樣希望構建與之前“授權服務的核心服務”相對應的服務。這些核心服務如下:

  • Model Mapper (ModelMapper) 不是核心服務,但我們將會在後續的所有核心服務中使用它,用於在實體對象和領域對象之間進行映射。
  • 註冊客户端倉庫 (RedisRegisteredClientRepository) 結合了 OAuth2RegisteredClientRepositoryOAuth2RegisteredClientModelMapper 類,用於持久化 RegisteredClient 對象。
  • 授權同意服務 (RedisOAuth2AuthorizationConsentService) 結合了 OAuth2UserConsentRepositoryOAuth2UserConsentModelMapper 類,用於持久化 OAuth2AuthorizationConsent 對象。
  • 授權服務 (RedisOAuth2AuthorizationService) 結合了 OAuth2AuthorizationGrantAuthorizationRepositoryOAuth2AuthorizationGrantAuthorizationModelMapper 類,用於持久化 OAuth2Authorization 對象。

可以從 GitHub 上找到這些服務的實現代碼。

3.4. 使用 Redis 配置 Spring Authorization 服務

最後,我們需要創建 Spring 配置文件,以啓用 Spring Authorization 服務的核心服務。

SecurityConfig 類應包含與基礎項目相同的 Bean。 我們註冊並稍後在演示中使用用户憑據為用户名“admin”,密碼“password”。 讓我們看一下 Redis 的配置:

@Configuration(proxyBeanMethods = false)
@EnableRedisRepositories
public class RedisConfig {
    // fields omitted

    @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 服務器
  • JedisConnectionFactoryRedisTemplate Bean 用於連接到 Redis
  • RedisCustomConversions Bean 用於對象到哈希的轉換,以便將數據持久化到 Redis
  • RedisRegisteredClientRepository 用於設置 registeredClientRepository Bean,然後註冊客户端,如下一部分將解釋的
  • RedisOAuth2AuthorizationService 作為 authorizationService Bean 註冊,並設置了適當的 Repository
  • 同樣,RedisOAuth2AuthorizationConsentService 作為 authorizationConsentService Bean 註冊

3.5. 已註冊客户端

當我們使用 Spring OAuth Security 且使用默認的內存持久化時,可以使用屬性定義已註冊客户端。 這是在 Spring Authorization Service 從版本 3.1.0 開始的內置功能,它使用 OAuth2AuthorizationServerProperties 類。

但是,由於我們使用 Redis 實現 Spring Authorization Service 的核心服務,更具體地説,我們使用自定義的 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-codearticles-client-oidc 進行身份驗證。無論哪種方式,他們都需要提供用户名和密碼(“admin”、“password”,在 UserDetailsService Bean 中設置)。

這樣,他們就向授權服務器進行身份驗證,並獲取訪問 articles-client 的權限。如果用户選擇第一個選項進行身份驗證,登錄成功後,他們會被重定向回該頁面,然後點擊第二個鏈接才能導航到 articles 頁面。如果用户選擇了第二個選項,則一切自動完成:

在圖像中,我們可以看到,在成功登錄後,用户可以訪問資源。我們還可以看到添加到瀏覽器的 Cookie。

另外需要注意的是,由於我們實現了 Spring 授權服務的核心服務,並且使用了 Redis,因此現在我們可以看到 Redis 服務器中包含有關活動會話以及更多信息:

oauth2_registered_client 對象是在登錄之前在 Redis 中唯一存在的對象。登錄成功後,其餘所有數據都是我們存儲的。

5. 結論

本文介紹了 Spring Authorization Service 的核心服務以及與 Redis 的使用。我們回顧了從默認的內存存儲切換到使用 Redis 時需要修改的組件。最後,我們觀察了授權用户訪問資源的過程,以及 Authorization Server 如何在 Redis 中存儲令牌。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.