OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client
作者:mmseoamin日期:2023-12-25

title: OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client

date: 2023-03-27 01:41:26

tags:

  • OAuth2.0
  • Spring Authorization Server

    categories:

  • 开发实践

    cover: https://cover.png

    feature: false


    1. 授权服务器

    目前 Spring 生态中的 OAuth2 授权服务器是 Spring Authorization Server,原先的 Spring Security OAuth 已经停止更新

    1.1 引入依赖

    这里的 spring-security-oauth2-authorization-server 用的是 0.4.0 版本,适配 JDK 1.8,Spring Boot 版本为 2.7.7

    
            
                org.springframework.boot
                spring-boot-starter-security
            
            
                org.springframework.security
                spring-security-oauth2-authorization-server
            
            
                org.springframework.boot
                spring-boot-starter-web
            
            
                org.springframework.boot
                spring-boot-starter-thymeleaf
            
            
                org.springframework.boot
                spring-boot-starter-jdbc
            
            
                mysql
                mysql-connector-java
            
    
    

    1.2 配置类

    可以参考官方的 Samples:spring-authorization-server/samples

    1.2.1 最小配置

    官网最小配置 Demo 地址:Getting Started

    官网最小配置如下,通过添加该配置类,启动项目,这就能够完成 OAuth2 的授权

    @Configuration
    public class SecurityConfig {
    	@Bean 
    	@Order(1)
    	public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
    			throws Exception {
    		OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
    		http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
    			.oidc(Customizer.withDefaults());	// Enable OpenID Connect 1.0
    		http
    			// Redirect to the login page when not authenticated from the
    			// authorization endpoint
    			.exceptionHandling((exceptions) -> exceptions
    				.authenticationEntryPoint(
    					new LoginUrlAuthenticationEntryPoint("/login"))
    			)
    			// Accept access tokens for User Info and/or Client Registration
    			.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
    		return http.build();
    	}
    	@Bean 
    	@Order(2)
    	public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
    			throws Exception {
    		http
    			.authorizeHttpRequests((authorize) -> authorize
    				.anyRequest().authenticated()
    			)
    			// Form login handles the redirect to the login page from the
    			// authorization server filter chain
    			.formLogin(Customizer.withDefaults());
    		return http.build();
    	}
    	@Bean 
    	public UserDetailsService userDetailsService() {
    		UserDetails userDetails = User.withDefaultPasswordEncoder()
    				.username("user")
    				.password("password")
    				.roles("USER")
    				.build();
    		return new InMemoryUserDetailsManager(userDetails);
    	}
    	@Bean 
    	public RegisteredClientRepository registeredClientRepository() {
    		RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
    				.clientId("messaging-client")
    				.clientSecret("{noop}secret")
    				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
    				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
    				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
    				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
    				.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
    				.redirectUri("http://127.0.0.1:8080/authorized")
    				.scope(OidcScopes.OPENID)
    				.scope(OidcScopes.PROFILE)
    				.scope("message.read")
    				.scope("message.write")
    				.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
    				.build();
    		return new InMemoryRegisteredClientRepository(registeredClient);
    	}
    	@Bean 
    	public JWKSource jwkSource() {
    		KeyPair keyPair = generateRsaKey();
    		RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
    		RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
    		RSAKey rsaKey = new RSAKey.Builder(publicKey)
    				.privateKey(privateKey)
    				.keyID(UUID.randomUUID().toString())
    				.build();
    		JWKSet jwkSet = new JWKSet(rsaKey);
    		return new ImmutableJWKSet<>(jwkSet);
    	}
    	private static KeyPair generateRsaKey() { 
    		KeyPair keyPair;
    		try {
    			KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
    			keyPairGenerator.initialize(2048);
    			keyPair = keyPairGenerator.generateKeyPair();
    		}
    		catch (Exception ex) {
    			throw new IllegalStateException(ex);
    		}
    		return keyPair;
    	}
    	@Bean 
    	public JwtDecoder jwtDecoder(JWKSource jwkSource) {
    		return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    	}
    	@Bean 
    	public AuthorizationServerSettings authorizationServerSettings() {
    		return AuthorizationServerSettings.builder().build();
    	}
    }
    

    在上面的 Demo 里,将所有配置都写在了一个配置类 SecurityConfig 里,实际上 Spring Authorization Server 还提供了一种实现最小配置的默认配置形式,就是通过 OAuth2AuthorizationServerConfiguration 这个类,源码如下:

    @Configuration(proxyBeanMethods = false)
    public class OAuth2AuthorizationServerConfiguration {
    	@Bean
    	@Order(Ordered.HIGHEST_PRECEDENCE)
    	public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
    		applyDefaultSecurity(http);
    		return http.build();
    	}
    	// @formatter:off
    	public static void applyDefaultSecurity(HttpSecurity http) throws Exception {
    		OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
    				new OAuth2AuthorizationServerConfigurer();
    		RequestMatcher endpointsMatcher = authorizationServerConfigurer
    				.getEndpointsMatcher();
    		http
    			.requestMatcher(endpointsMatcher)
    			.authorizeRequests(authorizeRequests ->
    				authorizeRequests.anyRequest().authenticated()
    			)
    			.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
    			.apply(authorizationServerConfigurer);
    	}
    	// @formatter:on
    	public static JwtDecoder jwtDecoder(JWKSource jwkSource) {
    		Set jwsAlgs = new HashSet<>();
    		jwsAlgs.addAll(JWSAlgorithm.Family.RSA);
    		jwsAlgs.addAll(JWSAlgorithm.Family.EC);
    		jwsAlgs.addAll(JWSAlgorithm.Family.HMAC_SHA);
    		ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>();
    		JWSKeySelector jwsKeySelector =
    				new JWSVerificationKeySelector<>(jwsAlgs, jwkSource);
    		jwtProcessor.setJWSKeySelector(jwsKeySelector);
    		// Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it instead
    		jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
    		});
    		return new NimbusJwtDecoder(jwtProcessor);
    	}
    	@Bean
    	RegisterMissingBeanPostProcessor registerMissingBeanPostProcessor() {
    		RegisterMissingBeanPostProcessor postProcessor = new RegisterMissingBeanPostProcessor();
    		postProcessor.addBeanDefinition(AuthorizationServerSettings.class, () -> AuthorizationServerSettings.builder().build());
    		return postProcessor;
    	}
    }
    

    这里注入一个叫做 authorizationServerSecurityFilterChain 的 bean,其实对比一下可以看出,这和最小配置的实现基本是相同的。有了这个 bean,就会支持如下协议端点:

    • OAuth2 Authorization endpoint
    • OAuth2 Token endpoint
    • OAuth2 Token Introspection endpoint
    • OAuth2 Token Revocation endpoint
    • OAuth2 Authorization Server Metadata endpoint
    • JWK Set endpoint
    • OpenID Connect 1.0 Provider Configuration endpoint
    • OpenID Connect 1.0 UserInfo endpoint

      接下来使用 OAuth2AuthorizationServerConfiguration 这个类来实现一个 Authorization Server,将 Spring Security 和 Authorization Server 的配置分开,Spring Security 使用 SecurityConfig 类,创建一个新的Authorization Server 配置类 AuthorizationServerConfig

      OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client,第1张

      1.2.2 ServerSecurityConfig

      @EnableWebSecurity
      @Configuration(proxyBeanMethods = false)
      public class ServerSecurityConfig {
          @Resource
          private DataSource dataSource;
          /**
           * Spring Security 的过滤器链,用于 Spring Security 的身份认证
           */
          @Bean
          SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
              http.authorizeHttpRequests(authorize -> authorize
                              // 配置放行的请求
                              .antMatchers("/api/**", "/login").permitAll()
                              // 其他任何请求都需要认证
                              .anyRequest().authenticated()
                      )
                      // 设置登录表单页面
                      .formLogin(formLoginConfigurer -> formLoginConfigurer.loginPage("/login"));
              return http.build();
          }
        
      //    @Bean
      //    public UserDetailsService userDetailsService() {
      //        return new JdbcUserDetailsManager(dataSource);
      //    }
          @Bean
          UserDetailsManager userDetailsManager() {
              return new JdbcUserDetailsManager(dataSource);
          }
      }
      

      Spring Authorization Server 默认是支持内存和 JDBC 两种存储模式的,内存模式只适合简单的测试,所以这里使用 JDBC 存储模式。在 1.2.1 最小配置那节里注入 UserDetailsService 这个 Bean 使用的是 InMemoryUserDetailsManager,表示内存模式,这里使用 JdbcUserDetailsManager 表示 JDBC 模式

      而这两个类都属于 UserDetailsManager 接口的实现类,并且后续我们需要使用到 userDetailsManager.createUser(userDetails) 方法来添加用户,因此这里需要注入 UserDetailsManager 这个 Bean,由于返回的都是 JdbcUserDetailsManager,因此可以注释掉 UserDetailsService 这个 Bean 的注入

      OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client,第2张

      1.2.3 AuthorizationServerConfig

      该类部分配置可以参照前面提到的 OAuth2AuthorizationServerConfiguration 类来配置,同样使用 JDBC 存储模式

      @Configuration(proxyBeanMethods = false)
      public class AuthorizationServerConfig {
          private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
          @Bean
          @Order(Ordered.HIGHEST_PRECEDENCE)
          public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
              // 定义授权服务配置器
              OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer();
              configurer
                      // 自定义授权页面
                      .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
                      // Enable OpenID Connect 1.0, 启用 OIDC 1.0
                      .oidc(Customizer.withDefaults());
              // 获取授权服务器相关的请求端点
              RequestMatcher endpointsMatcher = configurer.getEndpointsMatcher();
              http
                      // 拦截对授权服务器相关端点的请求
                      .requestMatcher(endpointsMatcher)
                      // 拦载到的请求需要认证
                      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
                      // 忽略掉相关端点的 CSRF(跨站请求): 对授权端点的访问可以是跨站的
                      .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
                      .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
                      // 访问端点时表单登录
                      .formLogin()
                      .and()
                      // 应用授权服务器的配置
                      .apply(configurer);
              return http.build();
          }
          /**
           * 注册客户端应用, 对应 oauth2_registered_client 表
           */
          @Bean
          public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
              return new JdbcRegisteredClientRepository(jdbcTemplate);
          }
          /**
           * 令牌的发放记录, 对应 oauth2_authorization 表
           */
          @Bean
          public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
              return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
          }
          /**
           * 把资源拥有者授权确认操作保存到数据库, 对应 oauth2_authorization_consent 表
           */
          @Bean
          public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
              return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
          }
          /**
           * 加载 JWT 资源, 用于生成令牌
           */
          @Bean
          public JWKSource jwkSource() {
              KeyPair keyPair;
              try {
                  KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
                  keyPairGenerator.initialize(2048);
                  keyPair = keyPairGenerator.generateKeyPair();
              } catch (Exception ex) {
                  throw new IllegalStateException(ex);
              }
              RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
              RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
              RSAKey rsaKey = new RSAKey.Builder(publicKey)
                      .privateKey(privateKey)
                      .keyID(UUID.randomUUID().toString())
                      .build();
              JWKSet jwkSet = new JWKSet(rsaKey);
              return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
          }
          /**
           * JWT 解码
           */
          @Bean
          public JwtDecoder jwtDecoder(JWKSource jwkSource) {
              return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
          }
          /**
           * AuthorizationServerS 的相关配置
           */
          @Bean
          public AuthorizationServerSettings authorizationServerSettings() {
              return AuthorizationServerSettings.builder().build();
          }
      }
      

      1.3 创建数据库表

      一共包括 5 个表,其中 Spring Security 相关的有 2 个表,user 和 authorities,用户表和权限表,该表的建表 SQL 在

      org\springframework\security\core\userdetails\jdbc\users.ddl

      SQL 可能会有一些问题,根据自己使用的数据库进行更改

      create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null);
      create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
      create unique index ix_auth_username on authorities (username,authority);
      

      Spring authorization Server 有 3 个表,建表 SQL 在:

      org\springframework\security\oauth2\server\authorization\oauth2-authorization-consent-schema.sql

      org\springframework\security\oauth2\server\authorization\oauth2-authorization-schema.sql

      org\springframework\security\oauth2\server\authorization\client\oauth2-registered-client-schema.sql

      CREATE TABLE oauth2_authorization_consent (
          registered_client_id varchar(100) NOT NULL,
          principal_name varchar(200) NOT NULL,
          authorities varchar(1000) NOT NULL,
          PRIMARY KEY (registered_client_id, principal_name)
      );
      
      /*
      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,
          authorized_scopes varchar(1000) DEFAULT 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)
      );
      
      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)
      );
      

      创建完成后的数据库表如下:

      OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client,第3张

      1.4 自定义登录和授权页面

      在项目 resource 目录下创建一个 templates 文件夹,然后创建 login.html 和 consent.html,登录页面的配置在 1.2.2 中配置好了,授权页面的配置在 1.2.3 中配置好了

      OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client,第4张

      登录页面 login.html

      
      
      
          
          
          Spring Security Example