Spring Authorization Server(AS)从 Mysql 中读取客户端、用户

虚幻大学 xuhss 409℃ 0评论

? 优质资源分享 ?

学习路线指引(点击解锁) 知识定位 人群定位
? Python实战微信订餐小程序 ? 进阶级 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
?Python量化交易实战? 入门级 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

Spring AS 持久化

jdk version: 17
spring boot version: 2.7.0
spring authorization server:0.3.0
mysql version: 8.x

在 [[spring authorization server 实现授权中心]] 中实现了基础的演示功能。本文包含的内容有:

  1. 在 mysql 中保存客户端信息
  2. 在 mysql 中保存用户信息

创建数据表

查看 [[spring authorization server 实现授权中心#AuthorizationServerConfig]] 可以看到以下配置,这里定义了一个嵌入数据 Bean,包含 3 条数据库脚本。分别用于创建

  • oauth2_registered_client
  • oauth2_authorization_consent
  • oauth2_authorization
@Bean  
public EmbeddedDatabase embeddedDatabase() {  
    return new EmbeddedDatabaseBuilder()  
            .generateUniqueName(true)  
            .setType(EmbeddedDatabaseType.H2)  
            .setScriptEncoding("UTF-8")  
            .addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")  
            .addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql")  
            .addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")  
            .build();  
}

oauth2_registered_client

CREATE TABLE oauth2_registered_client (

id varchar(100) NOT NULL,

client_id varchar(100) NOT NULL,

client_id_issued_at timestamp DEFAULT CURRENT\_TIMESTAMP NOT NULL,

client_secret varchar(200) DEFAULT NULL,

client_secret_expires_at timestamp DEFAULT NULL,

client_name varchar(200) NOT NULL,

client_authentication_methods varchar(1000) NOT NULL,

authorization_grant_types varchar(1000) NOT NULL,

redirect_uris varchar(1000) DEFAULT NULL,

scopes varchar(1000) NOT NULL,

client_settings varchar(2000) NOT NULL,

token_settings varchar(2000) NOT NULL,

PRIMARY KEY (id)

);

打开 mysql,创建 auth-center 数据库,执行 [[#oauth2_registered_client]] 脚本。

oauth2_authorization

用户认证时需要此表。

/*

IMPORTANT:

If using PostgreSQL, update ALL columns defined with 'blob' to 'text',

as PostgreSQL does not support the 'blob' data type.

*/

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 blob 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 blob 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 blob 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 blob 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 blob DEFAULT NULL,

PRIMARY KEY (id)

);

配置 application.yml

  1. build.gradle 中依赖更改如下所示

    • 添加 mysql 驱动
    • 去掉 H2 相关依赖

...

dependencies{
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.security:spring-security-oauth2-authorization-server:0.3.0'
implementation 'org.springframework.boot:spring-boot-starter-actuator'

compileOnly 'org.projectlombok:lombok'  
developmentOnly 'org.springframework.boot:spring-boot-devtools'  
runtimeOnly 'mysql:mysql-connector-java'  

annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'  
annotationProcessor 'org.projectlombok:lombok'  

testImplementation 'org.springframework.boot:spring-boot-starter-test'  
testImplementation 'org.springframework.security:spring-security-test'

}

...

2. 更改 application.yml 如下

server:
port: 9000

logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.security: INFO
org.springframework.security.oauth2: INFO

spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/auth-center?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
port: 9000

logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.security: INFO
org.springframework.security.oauth2: INFO

spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/auth-center?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456

client:
registers:


### 读取配置 ConfigurationProperties

...
@ConfigurationProperties(prefix = "client")
@ConstructorBinding
public record RegisterClientConfig(List registers) {

public record Register(String clientId, String clientSecret, String authenticationMethod, List grantTypes, 

List scopes, List redirectUris) {
}
}


### 添加 Member 对象

@Getter
@Setter
@ToString
@AllArgsConstructor
@RequiredArgsConstructor
public class Member implements UserDetails {

private Long id;  

private String loginAccount;  

private String password;  

@Transient  
private List authorities; 

@Override
public Collection <span class="hljs-keyword"extends GrantedAuthority> getAuthorities() {
return AuthorityUtils.createAuthorityList("read", "write");
}

@Override
public String getPassword() {
return password;
}

@Override
public String getUsername() {
return loginAccount;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}


### 添加 MbrRepository

@Repository
public interface MbrRepository extends CrudRepository {

Optional findByLoginAccount(String loginAccount);
}


### MbrService

public interface MbrService extends UserDetailsService {

}


### UserDetailsServiceImp

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImp implements MbrService {

private final MbrRepository mbrRepository;  

@Override  
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {  
    return mbrRepository.findByLoginAccount(username).orElseThrow(() -> new UsernameNotFoundException("用户不存在"));  
}  

}


### AuthorizationServerConfig

...
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {

@Bean  
@Order(Ordered.HIGHEST\_PRECEDENCE)  
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {  
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);  
    return http.formLogin(withDefaults()).build();  
}  

@Bean  
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {  
    return new JdbcRegisteredClientRepository(jdbcTemplate);  
}  

@Bean  
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {  
    return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);  
}  

@Bean  
public JWKSource jwkSource() { 

RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

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

}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
@RequiredArgsConstructor
public class AuthorizationServerConfig {

private final JdbcTemplate jdbcTemplate;
private final RegisterClientConfig clientConfig;
private final MbrService mbrService;

@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.exceptionHandling((exceptions) -%3E exceptions
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
);

return http.build();
}

@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.userDetailsService(mbrService)
.formLogin(withDefaults());
return http.build();
}

@Bean
public RegisteredClientRepository registeredClientRepository() {
return new JdbcRegisteredClientRepository(jdbcTemplate);
}

@Bean
public OAuth2AuthorizationService authorizationService(RegisteredClientRepository registeredClientRepository, PasswordEncoder passwordEncoder) {
clientConfig.registers().forEach(cfg -> {
RegisteredClient registeredClientFromDb = registeredClientRepository.findByClientId(cfg.clientId());
if (registeredClientFromDb != null) {
return;
}
RegisteredClient.Builder registerBuilder = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId(cfg.clientId())
.clientSecret(passwordEncoder.encode(cfg.clientSecret()))
.clientAuthenticationMethod(new ClientAuthenticationMethod(cfg.authenticationMethod()));
cfg.grantTypes().forEach(grantType -> registerBuilder.authorizationGrantType(new AuthorizationGrantType(grantType)));
cfg.redirectUris().forEach(registerBuilder::redirectUri);
cfg.scopes().forEach(registerBuilder::scope);
registeredClientRepository.save(registerBuilder.build());
});
JdbcOAuth2AuthorizationService jdbcOAuth2AuthorizationService = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
jdbcOAuth2AuthorizationService.setAuthorizationRowMapper(new RowMapper(registeredClientRepository));
return jdbcOAuth2AuthorizationService;
}

@Bean
public JWKSource%3CSecurityContext> jwkSource() {
RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

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

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

static class RowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper {
RowMapper(RegisteredClientRepository registeredClientRepository) {
super(registeredClientRepository);
getObjectMapper().addMixIn(Member.class, MemberMixin.class);
}
}

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonDeserialize(using = MemberDeserializer.class)
static class MemberMixin {
}

}


### EncoderConfig

@Configuration
public class EncoderConfig {

@Bean  
@ConditionalOnMissingBean(PasswordEncoder.class)  
public PasswordEncoder passwordEncoder() {  
    return new BCryptPasswordEncoder();  
}  

}


### MemberDeserializer

public class MemberDeserializer extends JsonDeserializer {

@Override
public Member deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
JsonNode jsonNode = mapper.readTree(jsonParser);
Long id = readJsonNode(jsonNode, "id").asLong();
String loginAccount = readJsonNode(jsonNode, "loginAccount").asText();
String password = readJsonNode(jsonNode, "password").asText();
List authorities = mapper.readerForListOf(GrantedAuthority.class).readValue(jsonNode.get("authorities"));
return new Member(id, loginAccount, password, authorities);
}

private JsonNode readJsonNode(JsonNode jsonNode, String field) {
return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
}
}


## 启动服务

@SpringBootApplication
@ConfigurationPropertiesScan
public class AuthCenterApplication {

public static void main(String[] args) {  
    SpringApplication.run(AuthCenterApplication.class, args);  
}  

}



## 总结

1. 目前 spring authorization server 版本是 0.3.0 ,在我看来仍然有诸多不完善的地方,但官方总不至于又实现一套 keycloak。
2. 0.3.0 版本发布之际,[官方文档](https://blog.csdn.net/biggbang) 也放出来了。

转载请注明:xuhss » Spring Authorization Server(AS)从 Mysql 中读取客户端、用户

喜欢 (24)

您必须 登录 才能发表评论!