? 优质资源分享 ?
学习路线指引(点击解锁) | 知识定位 | 人群定位 |
---|---|---|
? 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 实现授权中心]] 中实现了基础的演示功能。本文包含的内容有:
- 在 mysql 中保存客户端信息
- 在 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]] 脚本。
用户认证时需要此表。
/*
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
-
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:
- client-id: mobile-gateway-client
client-secret: "123456"
authentication-method: client_secret_basic
grant-types:- authorization_code
- refresh_token
- client_credentials
scopes: - openid
- message.read
- message.write
redirect-uris: - http://127.0.0.1:9100/login/oauth2/code/mobile-gateway-client-oidc
- http://127.0.0.1:9100/authorized
### 读取配置 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 中读取客户端、用户