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


    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:
        - 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<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 版本发布之际,官方文档 也放出来了。
  • 相关阅读:
    GPT4应用讲解,如何获取ChatGPT账号
    JAVA学习-三元操作符if-else
    【实践篇】领域驱动设计:DDD工程参考架构
    matlab读取hdf5格式的全球火灾排放数据库Global Fire Emissions Database(GFED)数据
    SSM整合 Spring SprintMVC Mybatis
    mathtype中的制表位设置、公式编号靠右对齐
    有关flask和装饰器的python学习
    R语言 批量导入数据
    基于php 进行每半小时钉钉预警
    centos获取服务器公网ip
  • 原文地址:https://www.cnblogs.com/Zhang-Xiang/p/16337468.html