美文网首页
Spring Security 持久化OAuth2客户端

Spring Security 持久化OAuth2客户端

作者: ReLive27 | 来源:发表于2022-07-02 22:09 被阅读0次

    Spring Security 持久化OAuth2客户端

    之前文章中介绍过了客户端通过向授权服务器(使用Spring Authorization Server)请求授权并访问资源服务器受保护资源。在创建OAuth2客户端服务时,客户端注册通常从application.yml文件中自动加载,Spring 自动配置使用OAuth2ClientPropertiesspring.security.oauth2.client.registration.[registrationId]创建一个ClientRegistration并实例化ClientRegistrationRepository

    以下Spring自动配置OAuth2ClientRegistrationRepositoryConfiguration代码如下:

    @Configuration(
        proxyBeanMethods = false
    )
    @EnableConfigurationProperties({OAuth2ClientProperties.class})
    @Conditional({ClientsConfiguredCondition.class})
    class OAuth2ClientRegistrationRepositoryConfiguration {
        OAuth2ClientRegistrationRepositoryConfiguration() {
        }
    
        @Bean
        @ConditionalOnMissingBean({ClientRegistrationRepository.class})
        InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) {
            List<ClientRegistration> registrations = new ArrayList(OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
            return new InMemoryClientRegistrationRepository(registrations);
        }
    }
    

    如您所见,ClientRegistrationRepository默认实现并仅有一个实现类是InMemoryClientRegistrationRepository,它将ClientRegistration存储在内存中,而在生产环境中此方式可能会有一定局限性。

    在本文中您将了解如何通过扩展ClientRegistrationRepository实现OAuth2客户端持久化。

    OAuth2客户端服务实现

    在本节中,您将创建一个简单的OAuth2客户端服务,并通过数据库存储OAuth2客户端信息,现在看代码!

    maven

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      <version>2.6.7</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jdbc</artifactId>
      <version>2.6.7</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
      <version>2.6.7</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-oauth2-client</artifactId>
      <version>2.6.7</version>
    </dependency>
    
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webflux</artifactId>
      <version>5.3.9</version>
    </dependency>
    <dependency>
      <groupId>io.projectreactor.netty</groupId>
      <artifactId>reactor-netty</artifactId>
      <version>1.0.9</version>
    </dependency>
    
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.21</version>
    </dependency>
    
    ...
    

    配置

    首先让我们通过application.yml配置服务端口信息和数据库连接信息:

    server:
      port: 8070
      
    spring:
      datasource:
        druid:
          db-type: mysql
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/persistence_oauth2_client?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
          username: <<username>> # 修改用户名
          password: <<password>> # 修改密码
    

    接下来我们根据ClientRegistration来创建数据库表用于存储OAuth2客户端信息:

    CREATE TABLE `oauth2_registered_client`
    (
        `registration_id`                 varchar(100)  NOT NULL,
        `client_id`                       varchar(100)  NOT NULL,
        `client_secret`                   varchar(200)  DEFAULT NULL,
        `client_authentication_method`    varchar(100)  NOT NULL,
        `authorization_grant_type`        varchar(100)  NOT NULL,
        `client_name`                     varchar(200)  DEFAULT NULL,
        `redirect_uri`                    varchar(1000) NOT NULL,
        `scopes`                          varchar(1000) NOT NULL,
        `authorization_uri`               varchar(1000) DEFAULT NULL,
        `token_uri`                       varchar(1000) NOT NULL,
        `jwk_set_uri`                     varchar(1000) DEFAULT NULL,
        `issuer_uri`                      varchar(1000) DEFAULT NULL,
        `user_info_uri`                   varchar(1000) DEFAULT NULL,
        `user_info_authentication_method` varchar(100)  DEFAULT NULL,
        `user_name_attribute_name`        varchar(100)  DEFAULT NULL,
        `configuration_metadata`          varchar(2000) DEFAULT NULL,
        PRIMARY KEY (`registration_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
    

    下面将是我们通过实现ClientRegistrationRepository扩展的JdbcClientRegistrationRepository

    public class JdbcClientRegistrationRepository implements ClientRegistrationRepository {
        private static final String COLUMN_NAMES = "registration_id,client_id,client_secret,client_authentication_method,authorization_grant_type,client_name,redirect_uri,scopes,authorization_uri,token_uri,jwk_set_uri,issuer_uri,user_info_uri,user_info_authentication_method,user_name_attribute_name,configuration_metadata";
        private static final String TABLE_NAME = "oauth2_registered_client";
        private static final String LOAD_CLIENT_REGISTERED_SQL = "SELECT " + COLUMN_NAMES + " FROM " + TABLE_NAME + " WHERE ";
        private static final String INSERT_CLIENT_REGISTERED_SQL = "INSERT INTO " + TABLE_NAME + "(" + COLUMN_NAMES + ") VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
        private static final String UPDATE_CLIENT_REGISTERED_SQL = "UPDATE " + TABLE_NAME + " SET client_id = ?,client_secret = ?,client_authentication_method = ?,authorization_grant_type = ?,client_name = ?,redirect_uri = ?,scopes = ?,authorization_uri = ?,token_uri = ?,jwk_set_uri = ?,issuer_uri = ?,user_info_uri = ?,user_info_authentication_method = ?,user_name_attribute_name = ? WHERE registration_id = ?";
        private final JdbcOperations jdbcOperations;
        private RowMapper<ClientRegistration> clientRegistrationRowMapper;
        private Function<ClientRegistration, List<SqlParameterValue>> clientRegistrationListParametersMapper;
    
    
        public JdbcClientRegistrationRepository(JdbcOperations jdbcOperations) {
            Assert.notNull(jdbcOperations, "JdbcOperations can not be null");
            this.jdbcOperations = jdbcOperations;
            this.clientRegistrationRowMapper = new ClientRegistrationRowMapper();
            this.clientRegistrationListParametersMapper = new ClientRegistrationParametersMapper();
        }
    
        @Override
        public ClientRegistration findByRegistrationId(String registrationId) {
            Assert.hasText(registrationId, "registrationId cannot be empty");
            return this.findBy("registration_id = ?", registrationId);
        }
    
        private ClientRegistration findBy(String filter, Object... args) {
            List<ClientRegistration> result = this.jdbcOperations.query(LOAD_CLIENT_REGISTERED_SQL + filter, this.clientRegistrationRowMapper, args);
            return !result.isEmpty() ? result.get(0) : null;
        }
    
    
        public void save(ClientRegistration clientRegistration) {
            Assert.notNull(clientRegistration, "clientRegistration cannot be null");
            ClientRegistration existingClientRegistration = this.findByRegistrationId(clientRegistration.getRegistrationId());
            if (existingClientRegistration != null) {
                this.updateRegisteredClient(clientRegistration);
            } else {
                this.insertClientRegistration(clientRegistration);
            }
        }
    
        private void updateRegisteredClient(ClientRegistration clientRegistration) {
            List<SqlParameterValue> parameterValues = this.clientRegistrationListParametersMapper.apply(clientRegistration);
            PreparedStatementSetter statementSetter = new ArgumentPreparedStatementSetter(parameterValues.toArray());
            this.jdbcOperations.update(UPDATE_CLIENT_REGISTERED_SQL, statementSetter);
        }
    
        private void insertClientRegistration(ClientRegistration clientRegistration) {
            List<SqlParameterValue> parameterValues = this.clientRegistrationListParametersMapper.apply(clientRegistration);
            PreparedStatementSetter statementSetter = new ArgumentPreparedStatementSetter(parameterValues.toArray());
            this.jdbcOperations.update(INSERT_CLIENT_REGISTERED_SQL, statementSetter);
        }
      
      //...省略部分代码
    }
    

    之后我们将创建SecurityConfig安全配置类,在此类中创建OAuth2 Client所需特定的Bean。首先我们将实例化上述自定义的JdbcClientRegistrationRepository

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository(JdbcTemplate jdbcTemplate) {
      return new JdbcClientRegistrationRepository(jdbcTemplate);
    }
    

    ClientRegistration:表示使用 OAuth 2.0 或 OpenID Connect (OIDC) 注册的客户端。它包含有关客户端的所有基本信息,例如客户端 ID、客户端机密、授权类型和各种 URI。

    ClientRegistrationRepository:这是一个包含ClientRegistrations并负责持久化。

    接下来配置OAuth2AuthorizedClient管理类OAuth2AuthorizedClientService

    @Bean
    public OAuth2AuthorizedClientService authorizedClientService(
      JdbcTemplate jdbcTemplate,
      ClientRegistrationRepository clientRegistrationRepository) {
      return new JdbcOAuth2AuthorizedClientService(jdbcTemplate, clientRegistrationRepository);
    }
    

    OAuth2AuthorizedClient:表示授权客户端。这是一个包含客户端注册但添加身份验证信息的组合类。

    OAuth2AuthorizedClientService:负责OAuth2AuthorizedClient在 Web 请求之间进行持久化。

    定义JdbcOAuth2AuthorizedClientService需要创建所需数据表,你可以在OAuth2 Client Schema中获取表定义:

    CREATE TABLE oauth2_authorized_client
    (
        client_registration_id  varchar(100)                            NOT NULL,
        principal_name          varchar(200)                            NOT NULL,
        access_token_type       varchar(100)                            NOT NULL,
        access_token_value      blob                                    NOT NULL,
        access_token_issued_at  timestamp                               NOT NULL,
        access_token_expires_at timestamp                               NOT NULL,
        access_token_scopes     varchar(1000) DEFAULT NULL,
        refresh_token_value     blob          DEFAULT NULL,
        refresh_token_issued_at timestamp     DEFAULT NULL,
        created_at              timestamp     DEFAULT CURRENT_TIMESTAMP NOT NULL,
        PRIMARY KEY (client_registration_id, principal_name)
    );
    

    接下来配置OAuth2AuthorizedClientRepository容器类:

    @Bean
    public OAuth2AuthorizedClientRepository authorizedClientRepository(
      OAuth2AuthorizedClientService authorizedClientService) {
      return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
    }
    

    OAuth2AuthorizedClientRepository:是一个容器类,用于在请求之间保存和持久化授权客户端。这里通过JdbcOAuth2AuthorizedClientService将客户端存储在数据库中。

    接下来实例化包含授权流程的逻辑的管理器类:

    @Bean
    OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,
                                                          OAuth2AuthorizedClientRepository authorizedClientRepository) {
    
      OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder
        .builder()
        .authorizationCode()
        .refreshToken()
        .build();
      DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
      authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
    
      return authorizedClientManager;
    }
    

    OAuth2AuthorizedClientManager:是包含处理授权流程的逻辑的管理器类。最重要的是,它使用OAuth2AuthorizedClientProvider处理不同授权类型和 OAuth 2.0 提供者的实际请求逻辑。它还委托OAuth2AuthorizedClientRepository在客户端授权成功或失败时调用成功或失败处理程序。

    现在让我们创建一个WebClient实例用于向资源服务器执行HTTP请求:

     @Bean
        WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
            ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
            return WebClient.builder()
                    .apply(oauth2Client.oauth2Configuration())
                    .build();
        }
    

    最后,我们将配置Spring Security安全配置:

     @Bean
        SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests(authorizeRequests ->
                            authorizeRequests.anyRequest().authenticated()
                    )
                    .formLogin(login -> {
                        login.loginPage("/login").permitAll();
                    })
                    .oauth2Client(withDefaults());
            return http.build();
        }
    

    这里配置所有请求需要认证授权,提供Form表单认证方式,并通过thymeleaf自定义登录模版,此处代码并不再本文讲解范围内,以下将不展示具体细节。

    访问资源列表

    我们将创建一个PersistenceClientController,并使用WebClient向资源服务器发起HTTP请求:

    @RestController
    public class PersistenceClientController {
        @Autowired
        private WebClient webClient;
    
        @GetMapping(value = "/client/test")
        public List<String> getArticles(@RegisteredOAuth2AuthorizedClient("messaging-client-authorization-code") OAuth2AuthorizedClient authorizedClient) {
            return this.webClient
                    .get()
                    .uri("http://127.0.0.1:8090/resource/article")
                    .attributes(oauth2AuthorizedClient(authorizedClient))
                    .retrieve()
                    .bodyToMono(List.class)
                    .block();
        }
    }
    

    在本文中,您看到了OAuth2客户端服务持久化到数据库的实现方法,对于其他授权服务器和资源服务器配置将不再讲解,如果您感兴趣可以参考 此文章将JWT与Spring Security OAuth2结合使用

    结论

    如果您对这篇文章有任何疑问,请在下面添加评论。与往常一样,本文中使用的源代码可在 GitHub 上获得。

    相关文章

      网友评论

          本文标题:Spring Security 持久化OAuth2客户端

          本文链接:https://www.haomeiwen.com/subject/mgyrbrtx.html