写点什么

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

作者:Zhang
  • 2022 年 6 月 02 日
  • 本文字数:7169 字

    阅读完需:约 24 分钟

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

Spring AS 持久化

jdk version: 17spring boot version: 2.7.0spring authorization server:0.3.0mysql 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 中依赖更改如下所示

  2. 添加 mysql 驱动

  3. 去掉 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'  }    ...
复制代码


  1. 更改 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)@RequiredArgsConstructorpublic 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 版本发布之际,官方文档 也放出来了。

发布于: 刚刚阅读数: 3
用户头像

Zhang

关注

还未添加个人签名 2020.12.27 加入

还未添加个人简介

评论

发布
暂无评论
Spring Authorization Server(AS) 从 Mysql 中读取客户端配置、用户_Java_Zhang_InfoQ写作社区