Skip to content
清晨的一缕阳光
返回

Spring Boot OAuth2 授权服务器

前言

OAuth2 是业界标准的授权协议。Spring Authorization Server 是 Spring 官方的 OAuth2 授权服务器实现。本文将介绍如何构建完整的 OAuth2 授权服务器。

快速开始

1. 添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

2. 配置授权服务器

@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {
    
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(
        HttpSecurity http
    ) throws Exception {
        
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        
        http
            .getConfigurer(OAuth2AuthorizationServerConfigurer.class)
            .oidc(Customizer.withDefaults())
            .and()
            .formLogin(Customizer.withDefaults());
        
        return http.build();
    }
    
    @Bean
    public RegisteredClientRepository registeredClientRepository(
        JdbcTemplate jdbcTemplate
    ) {
        RegisteredClient registeredClient = RegisteredClient.withId(
            UUID.randomUUID().toString()
        )
        .clientId("demo-client")
        .clientSecret("{noop}demo-secret")
        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
        .redirectUri("http://localhost:8080/login/oauth2/code/demo-client")
        .scope(OidcScopes.OPENID)
        .scope("read")
        .scope("write")
        .clientSettings(ClientSettings.builder()
            .requireAuthorizationConsent(true)
            .build())
        .build();
        
        JdbcRegisteredClientRepository clientRepository = 
            new JdbcRegisteredClientRepository(jdbcTemplate);
        
        clientRepository.save(registeredClient);
        
        return clientRepository;
    }
}

3. 配置用户详情

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/assets/**", "/login").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            );
        
        return http.build();
    }
    
    @Bean
    public UserDetailsService userDetailsService(
        PasswordEncoder passwordEncoder
    ) {
        UserDetails user = User.builder()
            .username("admin")
            .password(passwordEncoder.encode("admin123"))
            .roles("ADMIN")
            .build();
        
        return new InMemoryUserDetailsManager(user);
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

授权码模式

1. 授权流程

1. 用户访问客户端
2. 客户端重定向到授权服务器
   GET /oauth2/authorize?
       response_type=code&
       client_id=demo-client&
       redirect_uri=http://localhost:8080/callback&
       scope=read write&
       state=xyz123

3. 用户登录并授权
4. 授权服务器重定向回客户端
   GET /callback?code=AUTH_CODE&state=xyz123

5. 客户端用授权码换取 Token
   POST /oauth2/token
   grant_type=authorization_code&
   code=AUTH_CODE&
   redirect_uri=http://localhost:8080/callback

6. 返回 Access Token 和 Refresh Token

2. 客户端配置

@Configuration
public class OAuth2ClientConfig {
    
    @Bean
    public RegisteredClientRepository registeredClientRepository(
        JdbcTemplate jdbcTemplate
    ) {
        RegisteredClient registeredClient = RegisteredClient.withId(
            UUID.randomUUID().toString()
        )
        .clientId("web-client")
        .clientSecret("{bcrypt}$2a$10$...")
        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
        .redirectUri("http://localhost:3000/callback")
        .scope(OidcScopes.OPENID)
        .scope("read")
        .scope("write")
        .tokenSettings(TokenSettings.builder()
            .accessTokenTimeToLive(Duration.ofHours(1))
            .refreshTokenTimeToLive(Duration.ofDays(7))
            .build())
        .build();
        
        return new JdbcRegisteredClientRepository(jdbcTemplate);
    }
}

3. 数据库表

-- 客户端表
CREATE TABLE oauth2_registered_client (
    id varchar(100) NOT NULL,
    client_id varchar(100) NOT NULL,
    client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP,
    client_secret varchar(200),
    client_secret_expires_at timestamp,
    client_name varchar(200),
    client_authentication_methods varchar(1000),
    authorization_grant_types varchar(1000),
    redirect_uris varchar(1000),
    scopes varchar(1000),
    client_settings varchar(2000),
    token_settings varchar(2000),
    PRIMARY KEY (id)
);

-- 授权表
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),
    authorized_scopes varchar(1000),
    attributes blob,
    state varchar(500),
    authorization_code_value blob,
    authorization_code_issued_at timestamp,
    authorization_code_expires_at timestamp,
    authorization_code_metadata blob,
    refresh_token_value blob,
    refresh_token_issued_at timestamp,
    refresh_token_expires_at timestamp,
    refresh_token_metadata blob,
    access_token_value blob,
    access_token_issued_at timestamp,
    access_token_expires_at timestamp,
    access_token_metadata blob,
    access_token_type varchar(100),
    access_token_scopes varchar(1000),
    oidc_id_token_value blob,
    oidc_id_token_issued_at timestamp,
    oidc_id_token_expires_at timestamp,
    oidc_id_token_metadata blob,
    PRIMARY KEY (id)
);

密码模式

1. 配置密码模式

@Configuration
public class AuthorizationServerConfig {
    
    @Bean
    public RegisteredClientRepository registeredClientRepository(
        JdbcTemplate jdbcTemplate
    ) {
        RegisteredClient registeredClient = RegisteredClient.withId(
            UUID.randomUUID().toString()
        )
        .clientId("mobile-client")
        .clientSecret("{bcrypt}$2a$10$...")
        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        .authorizationGrantType(AuthorizationGrantType.PASSWORD)
        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
        .scope("read")
        .scope("write")
        .tokenSettings(TokenSettings.builder()
            .accessTokenTimeToLive(Duration.ofHours(1))
            .build())
        .build();
        
        return new JdbcRegisteredClientRepository(jdbcTemplate);
    }
}

2. 获取 Token

# 请求
curl -X POST http://localhost:8080/oauth2/token \
  -H "Authorization: Basic bW9iaWxlLWNsaWVudDptb2JpbGUtc2VjcmV0" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password&username=admin&password=admin123&scope=read"

# 响应
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "read"
}

客户端模式

1. 配置客户端模式

@Configuration
public class AuthorizationServerConfig {
    
    @Bean
    public RegisteredClientRepository registeredClientRepository(
        JdbcTemplate jdbcTemplate
    ) {
        RegisteredClient registeredClient = RegisteredClient.withId(
            UUID.randomUUID().toString()
        )
        .clientId("service-client")
        .clientSecret("{bcrypt}$2a$10$...")
        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
        .scope("read")
        .scope("write")
        .build();
        
        return new JdbcRegisteredClientRepository(jdbcTemplate);
    }
}

2. 获取 Token

# 请求
curl -X POST http://localhost:8080/oauth2/token \
  -H "Authorization: Basic c2VydmljZS1jbGllbnQ6c2VydmljZS1zZWNyZXQ=" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&scope=read"

# 响应
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "read"
}

JWT Token

1. 配置 JWT 编码器

@Configuration
public class AuthorizationServerConfig {
    
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        RSAKey rsaKey = Jwks.generateRsaKey();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> 
            jwkSelector.select(jwkSet);
    }
    
    @Bean
    public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
        return new NimbusJwtEncoder(jwkSource);
    }
    
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }
}

2. 自定义 Token 内容

@Configuration
public class AuthorizationServerConfig {
    
    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
        return context -> {
            if (context.getTokenType().getValue().equals("ACCESS_TOKEN")) {
                context.getClaims().claims(claims -> {
                    claims.put("roles", getRoles(context.getPrincipal()));
                    claims.put("tenant_id", getTenantId(context.getPrincipal()));
                });
            }
        };
    }
    
    private List<String> getRoles(Authentication principal) {
        return principal.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toList());
    }
}

资源服务器

1. 配置资源服务器

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults())
            )
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            );
        
        return http.build();
    }
    
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = 
            new JwtGrantedAuthoritiesConverter();
        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
        grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
        
        JwtAuthenticationConverter jwtAuthenticationConverter = 
            new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(
            grantedAuthoritiesConverter
        );
        
        return jwtAuthenticationConverter;
    }
}

2. 配置 JWT 解码器

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8080
          jwk-set-uri: http://localhost:8080/.well-known/jwks.json

最佳实践

1. 安全配置

@Configuration
public class AuthorizationServerConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/.well-known/**", "/oauth2/jwks").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
        
        return http.build();
    }
}

2. 密钥管理

# 生产环境使用环境变量
jwt:
  key:
    private: ${JWT_PRIVATE_KEY}
    public: ${JWT_PUBLIC_KEY}
@Bean
public JWKSource<SecurityContext> jwkSource() {
    String privateKey = System.getenv("JWT_PRIVATE_KEY");
    String publicKey = System.getenv("JWT_PUBLIC_KEY");
    
    RSAKey rsaKey = new RSAKey.Builder(parsePublicKey(publicKey))
        .privateKey(parsePrivateKey(privateKey))
        .keyID("key-1")
        .build();
    
    JWKSet jwkSet = new JWKSet(rsaKey);
    return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

3. 刷新 Token 轮换

@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
    return context -> {
        if (context.getTokenType().getValue().equals("REFRESH_TOKEN")) {
            context.getClaims().claims(claims -> {
                claims.put("token_type", "refresh");
                claims.put("use_count", 0);
            });
        }
    };
}

总结

OAuth2 要点:

OAuth2 是现代认证授权的标准协议。


分享这篇文章到:

上一篇文章
Deepseek为什么会让世界震惊
下一篇文章
Nacos 服务注册发现