实战:Spring Authorization Server
版本迁移重要说明
Spring Authorization Server 1.5.x 是最后一代独立版本。 从 Spring Security 7.0 起,Spring Authorization Server 的功能已合并进 Spring Security 主项目,不再作为单独的依赖存在。
1.5.x 对应 Spring Boot 3.5.x,artifactId 为 spring-security-oauth2-authorization-server
Spring Security 7.0 起,直接引入 spring-boot-starter-oauth2-authorization-server
本文以 1.5.x 为主,文末提供 7.0 迁移要点
配置方式说明
Spring Authorization Server 提供两种配置方式:
方式一(推荐):手动创建授权服务器 Filter Chain,灵活可扩展,本文采用此方式
方式二(快速体验):使用 @Import(OAuth2AuthorizationServerConfiguration.class) 自动配置,仅适合无需自定义的极简场景,但不能与手动 Filter Chain 同时使用
理论回顾
在使用 Spring Authorization Server 之前,先回顾「核心概念」中的四个角色如何映射到 Spring 的组件:
核心概念
Spring 组件
说明
Client
RegisteredClient
定义客户端的 ID、Secret、授权类型、Scope、Redirect URI 等
Authorization Server
AuthorizationServerSettings
配置授权端点、令牌端点、JWKS 端点的 URL
签名能力
JWKSource
提供 JWT 签名所需的密钥对
Resource Owner
UserDetailsService
提供用户认证信息(用户名、密码、角色)
这三个 Bean 加上 SecurityFilterChain 就构成了一个最小可用的授权服务器。下面通过"快速入门"演示如何配置。
🚀 快速入门
Maven 依赖
pom.xml <dependencies>
<dependency>
<groupId> org.springframework.boot</groupId>
<artifactId> spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId> org.springframework.boot</groupId>
<artifactId> spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Authorization Server 1.5.x(最后独立版本) -->
<dependency>
<groupId> org.springframework.security</groupId>
<artifactId> spring-security-oauth2-authorization-server</artifactId>
</dependency>
</dependencies>
最小化配置
AuthorizationServerConfig.java @Configuration
public class AuthorizationServerConfig {
// (1)!
@Bean
public RegisteredClientRepository registeredClientRepository () {
RegisteredClient client = RegisteredClient . withId ( UUID . randomUUID (). toString ())
. clientId ( "my-client" )
. clientSecret ( "{noop}my-secret" ) // (2)!
. clientAuthenticationMethod ( ClientAuthenticationMethod . CLIENT_SECRET_BASIC )
. authorizationGrantType ( AuthorizationGrantType . AUTHORIZATION_CODE )
. authorizationGrantType ( AuthorizationGrantType . REFRESH_TOKEN )
. authorizationGrantType ( AuthorizationGrantType . CLIENT_CREDENTIALS )
. redirectUri ( "http://localhost:8080/login/oauth2/code/my-client" )
. scope ( OidcScopes . OPENID )
. scope ( OidcScopes . PROFILE )
. scope ( "read" )
. clientSettings ( ClientSettings . builder ()
. requireProofKey ( true ) // (3)!
. build ())
. tokenSettings ( TokenSettings . builder ()
. accessTokenTimeToLive ( Duration . ofMinutes ( 30 ))
. refreshTokenTimeToLive ( Duration . ofDays ( 7 ))
. reuseRefreshTokens ( false ) // (4)!
. build ())
. build ();
return new InMemoryRegisteredClientRepository ( client );
}
// (5)!
@Bean
public JWKSource < SecurityContext > jwkSource () {
RSAKey rsaKey = generateRsa ();
JWKSet jwkSet = new JWKSet ( rsaKey );
return ( jwkSelector , securityContext ) -> jwkSelector . select ( jwkSet );
}
private static RSAKey generateRsa () {
KeyPair keyPair = generateRsaKey ();
RSAPublicKey publicKey = ( RSAPublicKey ) keyPair . getPublic ();
RSAPrivateKey privateKey = ( RSAPrivateKey ) keyPair . getPrivate ();
return new RSAKey . Builder ( publicKey )
. privateKey ( privateKey )
. keyID ( UUID . randomUUID (). toString ())
. build ();
}
private static KeyPair generateRsaKey () {
try {
KeyPairGenerator generator = KeyPairGenerator . getInstance ( "RSA" );
generator . initialize ( 2048 );
return generator . generateKeyPair ();
} catch ( NoSuchAlgorithmException ex ) {
throw new IllegalStateException ( ex );
}
}
@Bean
public AuthorizationServerSettings authorizationServerSettings () {
return AuthorizationServerSettings . builder ()
. issuer ( "http://localhost:9000" ) // (6)!
. build ();
}
}
注册客户端仓库(生产环境应使用 JdbcRegisteredClientRepository)
{noop} 表示明文密码,生产环境必须使用 BCrypt 编码
强制要求 PKCE,适用于公开客户端
禁用 Refresh Token 复用,启用 Rotation 机制
JWK 密钥源,生产环境应从文件或 KMS 加载固定密钥
Issuer 必须与客户端配置的 issuer-uri 一致
配置 Spring Security 以开放授权服务器端点
SecurityConfig.java @Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Order ( 1 ) // (1)!
public SecurityFilterChain authorizationServerSecurityFilterChain ( HttpSecurity http )
throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer . authorizationServer ();
http
. securityMatcher ( authorizationServerConfigurer . getEndpointsMatcher ())
. with ( authorizationServerConfigurer , authorizationServer ->
authorizationServer
. oidc ( withDefaults ()) // (2)!
)
. exceptionHandling ( exceptions ->
exceptions . defaultAuthenticationEntryPointFor (
new LoginUrlAuthenticationEntryPoint ( "/login" ),
new MediaTypeRequestMatcher ( MediaType . TEXT_HTML )
)
);
return http . build ();
}
@Bean
@Order ( 2 )
public SecurityFilterChain defaultSecurityFilterChain ( HttpSecurity http )
throws Exception {
http
. authorizeHttpRequests ( authorize ->
authorize . anyRequest (). authenticated ()
)
. formLogin ( withDefaults ()); // (3)!
return http . build ();
}
@Bean
public UserDetailsService userDetailsService () {
// 生产环境替换为数据库实现
// ⚠️ withDefaultPasswordEncoder() 在 Spring Security 6.x 已标注 @Deprecated
// 仅供快速演示,生产环境请使用 BCryptPasswordEncoder 或数据库存储
UserDetails user = User . builder ()
. username ( "admin" )
. password ( "{bcrypt}" + new BCryptPasswordEncoder (). encode ( "password" ))
. roles ( "USER" )
. build ();
return new InMemoryUserDetailsManager ( user );
}
}
授权服务器 Security 链优先级必须高于默认链
启用 OpenID Connect 1.0 支持
默认使用表单登录,生产环境可自定义登录页
🎨 自定义授权页面
默认情况下 Spring Authorization Server 提供一个简单的授权确认页面。通过 authorizationEndpoint() 可以替换为自定义页面:
AuthorizationServerConfig.java(授权页面自定义片段) http
. with ( authorizationServerConfigurer , authorizationServer ->
authorizationServer
. authorizationEndpoint ( authorizationEndpoint ->
authorizationEndpoint
. consentPage ( "/oauth2/consent" ) // (1)!
)
. oidc ( withDefaults ())
);
指定自定义授权确认页面的路径,需在对应 Controller 中处理该路由并返回 Thymeleaf/HTML 页面
ConsentController.java @Controller
public class ConsentController {
// 展示授权确认页面(列出请求的 scope 供用户勾选)
@GetMapping ( "/oauth2/consent" )
public String consent ( Principal principal , Model model ,
@RequestParam ( OAuth2ParameterNames . CLIENT_ID ) String clientId ,
@RequestParam ( required = false , value = OAuth2ParameterNames . SCOPE ) String scope , // (1)!
@RequestParam ( OAuth2ParameterNames . STATE ) String state ) {
model . addAttribute ( "clientId" , clientId );
// scope 可能为 null(客户端未请求任何 scope 时),需做空值防护
Set < String > scopes = scope != null
? new HashSet <> ( Arrays . asList ( scope . split ( " " )))
: Collections . emptySet ();
model . addAttribute ( "scopes" , scopes );
model . addAttribute ( "state" , state );
model . addAttribute ( "principalName" , principal . getName ());
return "consent" ; // 对应 templates/consent.html
}
}
required = false 防止 scope 参数为 null 时抛出 MissingServletRequestParameterException
🏷️ 自定义令牌 Claims
使用 OAuth2TokenCustomizer 向 Access Token 或 ID Token 中添加自定义声明:
TokenCustomizerConfig.java @Configuration
public class TokenCustomizerConfig {
@Bean
public OAuth2TokenCustomizer < JwtEncodingContext > tokenCustomizer () {
return context -> {
if ( context . getTokenType (). equals ( OAuth2TokenType . ACCESS_TOKEN )) {
// 向 Access Token 添加用户角色
Authentication principal = context . getPrincipal ();
Set < String > authorities = principal . getAuthorities (). stream ()
. map ( GrantedAuthority :: getAuthority )
. collect ( Collectors . toSet ());
context . getClaims (). claim ( "roles" , authorities );
}
if ( OidcParameterNames . ID_TOKEN . equals ( context . getTokenType (). getValue ())) {
// 向 ID Token 添加额外用户信息
context . getClaims (). claim ( "custom_claim" , "custom_value" );
}
};
}
}
⚙️ 配置 application.yml
application.yml server :
port : 9000
spring :
security :
user :
# 开发调试用,生产环境移除
name : admin
password : password
UserDetailsService Bean 的优先级
若已在代码中定义了 UserDetailsService Bean(如上方的 InMemoryUserDetailsManager),Spring Boot Auto-configuration 会忽略 spring.security.user 属性配置。application.yml 中的 spring.security.user 仅在没有 UserDetailsService Bean 时生效,用于快速测试。
🔄 Spring Security 7.0 迁移要点
从 Spring Authorization Server 1.5.x 迁移到 Spring Security 7.0 的主要变化:
变化项
1.5.x
7.0+
依赖 artifactId
spring-security-oauth2-authorization-server
spring-boot-starter-oauth2-authorization-server
OAuth2AuthorizationServerConfigurer
独立类
集成进 Spring Security DSL
@Import(OAuth2AuthorizationServerConfiguration.class)
极简场景可用(不推荐生产使用)
通过 Security Filter Chain 配置
上一篇: 威胁模型与攻击面
下一篇: 实战:客户端与资源服务器