前言
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 要点:
- ✅ 授权码模式 - 最安全的模式
- ✅ 密码模式 - 适合受信任客户端
- ✅ 客户端模式 - 服务间调用
- ✅ JWT Token - 无状态认证
- ✅ 资源服务器 - 验证 Token
- ✅ 最佳实践 - 密钥管理、刷新轮换
OAuth2 是现代认证授权的标准协议。