1. 概述
在本教程中,我们将学习 JSON Web Signature(JWS)和 JSON Web Key(JWK)的基本概念,并演示如何在 Spring Security OAuth2 应用中实现它们。
虽然 Spring 正在逐步将 OAuth2 功能迁移到 Spring Security 框架中,但本教程仍是一个理解这些规范基本原理的良好起点,无论你使用何种框架,这些知识都会在实际开发中派上用场。
我们将从基础讲起,理解 JWS 和 JWK 的作用,以及如何配置 Resource Server 使用 JWK 验证 JWT 签名。接着我们会深入分析 OAuth2 Boot 的底层机制,并配置一个支持 JWK 的 Authorization Server。
2. 理解 JWS 与 JWK 的整体概念
在开始之前,建议你先熟悉 OAuth 和 JWT 的基本知识(可参考我们之前的 OAuth 教程 和 JWT 教程),因为这些内容不在本教程的讨论范围内。
✅ JWS 是什么?
JWS 是 IETF 制定的规范,用于描述如何通过加密机制验证数据的完整性,特别是用于 JSON Web Token (JWT) 的签名。
JWT 的标准形式是 JWS,因为我们需要确保其声明(claims)没有被篡改。加密后的 JWT 则使用 JWE(JSON Web Encryption)结构表示。
✅ JWK 是什么?
JWK 是一种用于表示加密密钥的 JSON 结构。OAuth 认证服务器通常会提供一个 /jwks.json
接口(即 JWK Set),供 Resource Server 获取公钥来验证 JWT 的签名。
Resource Server 会使用 JWT header 中的 kid
(Key ID)字段,去 JWK Set 中查找对应的公钥。
2.1. 使用 JWK 的典型流程
如果你的应用作为 Resource Server,需要安全地提供资源,流程大致如下:
- 客户端在 Authorization Server 上注册
- 客户端通过 OAuth2 协议获取 Access Token(通常是 JWT)
- 客户端将 Token 发送给 Resource Server
- Resource Server 使用 JWK Set 中的公钥验证 Token 的签名及声明
- 验证通过后返回资源
3. Resource Server 中的 JWK 配置
在本节中,我们先配置一个 Resource Server,它通过 JWK Set 验证来自 Authorization Server 的 JWT。
3.1. Maven 依赖
在 pom.xml
中添加 Spring Security OAuth2 Autoconfigure 依赖:
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
确保版本与你使用的 Spring Boot 一致。
3.2. 启用 Resource Server
使用 @EnableResourceServer
注解启用 Resource Server 功能:
@SpringBootApplication
@EnableResourceServer
public class ResourceServerApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceServerApplication.class, args);
}
}
3.3. 配置 JWK Set URI
在 application.properties
中配置 JWK Set 接口地址:
security.oauth2.resource.jwk.key-set-uri=http://localhost:8081/sso-auth-server/.well-known/jwks.json
Spring Boot 5.1+ 也支持更现代的配置方式:
spring.security.oauth2.resourceserver.jwk-set-uri=http://localhost:8081/sso-auth-server/.well-known/jwks.json
3.4. Spring Boot 底层配置
当你配置了 JWK Set URI,Spring 会自动创建以下两个 Bean:
JwkTokenStore
:用于解码 JWT 并验证签名DefaultTokenServices
:使用 TokenStore 处理 Token
4. Authorization Server 中的 JWK Set 接口
接下来我们配置一个支持 JWK 的 Authorization Server。
4.1. 启用 Authorization Server
使用 @EnableAuthorizationServer
注解启用 OAuth2 授权服务器:
@Configuration
@EnableAuthorizationServer
public class JwkAuthorizationServerConfiguration {
// ...
}
在 application.properties
中配置客户端信息:
security.oauth2.client.client-id=bael-client
security.oauth2.client.client-secret=bael-secret
使用 curl 请求 Token:
curl bael-client:bael-secret@localhost:8081/sso-auth-server/oauth/token \
-d grant_type=client_credentials \
-d scope=any
默认返回的是随机字符串,不是 JWT:
"access_token": "af611028-643f-4477-9319-b5aa8dc9408f"
4.2. 返回 JWT 格式 Token
创建 JwtAccessTokenConverter
和 JwtTokenStore
:
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
return new JwtAccessTokenConverter();
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
再次请求 Token,返回的是 JWT 格式,结构为 header.payload.signature
:
"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey...XKH70VUHeafHLaUPVXZI9E9pbFxrJ35PqBvrymxtvGI"
默认使用 HS256
算法,即 HMAC-SHA256。
4.3. 使用对称签名的限制
对称签名使用同一个密钥进行签名和验证,因此密钥不能公开。如果想让 Resource Server 验证 Token,需要使用非对称签名。
4.4. 非对称签名:使用 Keystore
生成 RSA 密钥对:
keytool -genkeypair \
-alias bael-oauth-jwt \
-keyalg RSA \
-keypass bael-pass \
-keystore bael-jwt.jks \
-storepass bael-pass
将 bael-jwt.jks
放入项目 resources
目录。
配置 JwtAccessTokenConverter
使用密钥对:
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
ClassPathResource ksFile = new ClassPathResource("bael-jwt.jks");
KeyStoreKeyFactory ksFactory = new KeyStoreKeyFactory(ksFile, "bael-pass".toCharArray());
KeyPair keyPair = ksFactory.getKeyPair("bael-oauth-jwt");
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyPair);
return converter;
}
4.5. 添加 JWK Set 接口
Spring Security OAuth 默认不支持 JWK,需要添加依赖:
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>7.3</version>
</dependency>
创建 JWKSet
Bean:
@Bean
public JWKSet jwkSet() {
RSAKey.Builder builder = new RSAKey.Builder((RSAPublicKey) keyPair().getPublic())
.keyUse(KeyUse.SIGNATURE)
.algorithm(JWSAlgorithm.RS256)
.keyID("bael-key-id");
return new JWKSet(builder.build());
}
创建 JWK Set 接口:
@RestController
public class JwkSetRestController {
@Autowired
private JWKSet jwkSet;
@GetMapping("/.well-known/jwks.json")
public Map<String, Object> keys() {
return this.jwkSet.toJSONObject();
}
}
4.6. 在 JWT Header 中添加 kid
为了让 Resource Server 能通过 kid
找到对应的公钥,我们需要在 JWT Header 中添加该字段。
扩展 JwtAccessTokenConverter
:
public class JwtCustomHeadersAccessTokenConverter extends JwtAccessTokenConverter {
private Map<String, String> customHeaders = new HashMap<>();
final RsaSigner signer;
public JwtCustomHeadersAccessTokenConverter(Map<String, String> customHeaders, KeyPair keyPair) {
super();
super.setKeyPair(keyPair);
this.signer = new RsaSigner((RSAPrivateKey) keyPair.getPrivate());
this.customHeaders = customHeaders;
}
private JsonParser objectMapper = JsonParserFactory.create();
@Override
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String content;
try {
content = this.objectMapper.formatMap(getAccessTokenConverter()
.convertAccessToken(accessToken, authentication));
} catch (Exception ex) {
throw new IllegalStateException("Cannot convert access token to JSON", ex);
}
String token = JwtHelper.encode(content, this.signer, this.customHeaders).getEncoded();
return token;
}
}
配置 Bean:
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
Map<String, String> customHeaders = Collections.singletonMap("kid", "bael-key-id");
return new JwtCustomHeadersAccessTokenConverter(customHeaders, keyPair());
}
5. 总结
✅ 本教程涵盖了 JWS 和 JWK 的基本原理
✅ 演示了如何在 Spring Security OAuth2 中配置 Resource Server 使用 JWK 验证 JWT
✅ 实现了自定义 JWK Set 接口,解决了默认不支持 kid
的问题
✅ 配置了非对称签名,提升了 Token 的安全性
通过本教程,你可以掌握在 Spring 中使用 JWK 的完整流程,并为构建安全的 OAuth2 系统打下基础。