Lưu RegisteredClient vào database trong Spring Authorization Server

1146

Bài viết được sự cho phép của tác giả Nguyễn Hữu Khanh

Trong bài viết trước, mình đã hướng dẫn các bạn cách hiện thực một Authorization Server sử dụng Spring Authorization Server, nhưng thông tin về RegisteredClient trong bài viết này được lưu trong memory. Để lưu thông tin RegisteredClient vào database thì chúng ta sẽ làm như thế nào? Trong bài viết này, mình sẽ hướng dẫn các bạn làm điều này các bạn nhé!

  Bean, ApplicationContext, Spring Bean Life Cycle và Component scan
  Authentication trong Spring Security

Xem thêm các chương trình tuyển dụng Spring lương cao trên TopDev

Đầu tiên, mình cũng tạo mới một Spring Boot project với Web Starter, Security Starter, Data JPA, PostgreSQL Driver:

và Spring Authorization Server:

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.2.0</version>
</dependency>

để làm ví dụ.

Kết quả:

Mình sẽ cấu hình Spring Security như trong bài viết Hiện thực OAuth Authorization Server sử dụng Spring Authorization Server như sau:

package com.huongdanjava.springauthorizationserver;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity
public class SpringSecurityConfiguration {

@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
// @formatter:on

return http.build();
}

@Bean
public UserDetailsService users() {
// @formatter:off
UserDetails user = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("ADMIN").build();
// @formatter:on

return new InMemoryUserDetailsManager(user);
}

}


Còn cấu hình cho Authorization Server, mình cũng làm tương tự như bài viết Hiện thực OAuth Authorization Server sử dụng Spring Authorization Server này nhưng phần khai báo thông tin RegisteredClient mình sẽ làm sau:

package com.huongdanjava.springauthorizationserver;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
import org.springframework.security.web.SecurityFilterChain;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;

@Configuration
public class AuthorizationServerConfiguration {

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

return http.formLogin(Customizer.withDefaults()).build();
}

@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}

@Bean
public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);

return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

private static RSAKey generateRsa() throws NoSuchAlgorithmException {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();

// @formatter:off
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
// @formatter:on
}

private static KeyPair generateRsaKey() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);

return keyPairGenerator.generateKeyPair();
}

@Bean
public ProviderSettings providerSettings() {
// @formatter:off
return ProviderSettings.builder()
.issuer("http://localhost:8080")
.build();
// @formatter:on
}

@Bean
public TokenSettings tokenSettings() {
//@formatter:off
return TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(30L))
.build();
// @formatter:on
}
}

Để lưu thông tin RegisteredClient vào database, đầu tiên, chúng ta cần định nghĩa database structure để làm việc này.

Mặc định thì Spring Authorization Server cung cấp cho chúng ta script database để tạo database structure. Các bạn có thể copy chúng trong tập tin .jar của Spring Authorization Server:

Các bạn có thể vào Github của Spring Authorization Server ở đây để copy những tập tin này.

Mình sẽ sử dụng Flyway để quản lý database migration:

<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>

bằng cách copy những tập tin schema của Spring Authorization Server vào thư mục src/main/resources/db/migration như sau:

Trong script tạo table oauth2_authorization trong tập tin V1__oauth2-authorization-schema.sql có định nghĩa kiểu dữ liệu BLOB, có lẽ cho Oracle database:

CREATE TABLE oauth2_authorization (
id varchar(100) NOT NULL,
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorization_grant_type varchar(100) NOT NULL,
attributes varchar(4000) DEFAULT NULL,
state varchar(500) DEFAULT NULL,
authorization_code_value blob DEFAULT NULL,
authorization_code_issued_at timestamp DEFAULT NULL,
authorization_code_expires_at timestamp DEFAULT NULL,
authorization_code_metadata varchar(2000) DEFAULT NULL,
access_token_value blob DEFAULT NULL,
access_token_issued_at timestamp DEFAULT NULL,
access_token_expires_at timestamp DEFAULT NULL,
access_token_metadata varchar(2000) DEFAULT NULL,
access_token_type varchar(100) DEFAULT NULL,
access_token_scopes varchar(1000) DEFAULT NULL,
oidc_id_token_value blob DEFAULT NULL,
oidc_id_token_issued_at timestamp DEFAULT NULL,
oidc_id_token_expires_at timestamp DEFAULT NULL,
oidc_id_token_metadata varchar(2000) DEFAULT NULL,
refresh_token_value blob DEFAULT NULL,
refresh_token_issued_at timestamp DEFAULT NULL,
refresh_token_expires_at timestamp DEFAULT NULL,
refresh_token_metadata varchar(2000) DEFAULT NULL,
PRIMARY KEY (id)
);

Nếu các bạn đang sử dụng PostgreSQL database như mình thì cần phải đổi sang kiểu BYTEA các bạn nhé! Không thì chạy database migration sẽ bị lỗi.

Khai báo Datasource để chạy database migration như sau:

spring.datasource.url=jdbc:postgresql://localhost:5432/authorization_server
spring.datasource.username=khanh
spring.datasource.password=1

Giờ thì các bạn có thể định nghĩa RegisteredClient trong database, ví dụ như sau:

@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
// @formatter:off
RegisteredClient registeredClient = RegisteredClient.withId("e4a295f7-0a5f-4cbc-bcd3-d870243d1b05")
.clientId("huongdanjava1")
.clientSecret("123")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.tokenSettings(tokenSettings())
.build();
// @formatter:on

JdbcRegisteredClientRepository registeredClientRepository =
new JdbcRegisteredClientRepository(jdbcTemplate);
registeredClientRepository.save(registeredClient);

return registeredClientRepository;
}

Ở đây, mình định nghĩa một RegisteredClient với grant type là client_credentials với ID cố định để mỗi khi start app, không có duplicate record trong database. Tuỳ theo nhu cầu thì các bạn hãy viết code tương ứng nhé!

Chúng ta sẽ sử dụng đối tượng JdbcRegisteredClientRepository để lưu thông tin RegisteredClient này. Tham số khi khởi tạo đối tượng JdbcRegisteredClientRepository là JdbcTemplate.

Lúc này, nếu chạy ứng dụng lên các bạn sẽ thấy trong table oauth2_registered_client, một record mới của RegisteredClient mà mình đã khai báo ở trên được insert vào:

Các bạn cũng nên để ý là client secret được mã hoá sử dụng class DelegatingPasswordEncoder với thuật toán bcrypt. Hiện tại chúng ta chưa thể khai báo thuật toán mà mình muốn!

Xong rồi đó các bạn, nếu bây giờ các bạn chạy ứng dụng và lấy token của clientId ở trên, các bạn sẽ thấy kết quả như sau: