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 的整体概念

bael 1239 image simple 1

在开始之前,建议你先熟悉 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,需要安全地提供资源,流程大致如下:

  1. 客户端在 Authorization Server 上注册
  2. 客户端通过 OAuth2 协议获取 Access Token(通常是 JWT)
  3. 客户端将 Token 发送给 Resource Server
  4. Resource Server 使用 JWK Set 中的公钥验证 Token 的签名及声明
  5. 验证通过后返回资源

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

创建 JwtAccessTokenConverterJwtTokenStore

@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 系统打下基础。