在previous article中,介绍了客户端请求从授权服务器(使用Spring Authorization Server)授权,并访问资源服务器的受保护资源。创建OAuth2客户端服务时,通常从application.yml
文件自动加载客户端注册,Spring Auto-Configuration使用OAuth2ClientProperties
创建ClientRegistration
并实现ClientRegistrationRepository
。
以下春季自动配置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
,它将客户端列置存储在内存中,并且此方法可能在生产环境中具有某些限制。
在本文中,您将学习如何通过扩展客户端registrationRepository来实现OAuth2客户端的持久性。
ðâ€注意:如果您不想阅读直到最后,您可以在此处查看source code。
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>> # Remember to change the username
password: <<password>> # Remember to change the 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;
下面我们实现 jdbcclientregistrationrepository clientregistrationrepository 扩展:
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客户端要求的特定bean。首先,我们将实例化上述自定义 jdbcclientregistrationrepository :
@Bean
public ClientRegistrationRepository clientRegistrationRepository(JdbcTemplate jdbcTemplate) {
return new JdbcClientRegistrationRepository(jdbcTemplate);
}
clientregistration :指示使用OAUTH 2.0或OpenID Connect(OIDC)注册的客户端。它包含有关客户端的所有基本信息,例如客户端ID,客户端秘密,授权类型和各种URI。
clientRegistrationRepository :这是一个包含 clientregsistrations 的存储库,并负责持久性。
下一个配置OAuth2AuthorizedClient Management类 oauth2authorizedClientservice :
@Bean
public OAuth2AuthorizedClientService authorizedClientService(
JdbcTemplate jdbcTemplate,
ClientRegistrationRepository clientRegistrationRepository) {
return new JdbcOAuth2AuthorizedClientService(jdbcTemplate, clientRegistrationRepository);
}
oauth2authorizedclient :指示授权客户端。这是一个包含客户端注册但添加身份验证信息的复合类。
oauth2authorizedClientservice :负责在Web请求之间持续kude10。
要定义 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 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();
}
在此处配置所有请求以需要身份验证和授权,提供表单身份验证方法,并通过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客户端服务持久性的实现方法。我不会解释其他授权服务器和资源服务器的配置。如果您有兴趣,可以参考本文Combining JWT with Spring Security OAuth2。
结论
一如既往,本文中使用的源代码可用on GitHub。