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

相关文章