Spring Authorization Server(AS)从 Mysql 中读取客户端、用户
阅读原文时间:2022年06月02日阅读:1

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.2.3'
      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](<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

    client:
    registers:
    - client-id: mobile-gateway-client
    client-secret: "{noop}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<Register> registers) {  

    public record Register(String clientId, String clientSecret, String authenticationMethod, List<String> grantTypes,
                           List<String> scopes, List<String> redirectUris) {
    }
}

添加 Member 对象

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

    private Long id;  

    private String loginAccount;  

    private String password;  

    @Transient
    private List<GrantedAuthority> authorities;  

    @Override
    public Collection<? 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<Member, Long> {  

    Optional<Member> 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<SecurityContext> 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<SecurityContext> 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<Member> {  

    @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<GrantedAuthority> 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 版本发布之际,官方文档 也放出来了。