- Published on
如何用 Vault 保护和管理 Spring Authorization Server JWT 密钥
- Authors
- Name
- ReLive27
简介
在现代应用中,安全性是首要考虑因素,特别是在涉及用户身份认证和授权的服务中。JWT(JSON Web Token)在 OAuth 2.0 和 OpenID Connect 标准中扮演了重要角色。作为一种加密令牌,JWT 通常用于标识和验证用户或客户端的身份,不管是使用对称加密还是非对称加密算法生成JWT,对于密钥或私钥的安全存储始终是最关键的问题。本文将介绍如何使用 HashiCorp Vault 来保护和管理 Spring Authorization Server 构建的授权服务的 JWT 密钥,通过 Vault 的密钥管理功能,确保 JWT 密钥的安全性。
Vault 简介
Vault 是一个专为密钥和机密数据管理而设计的工具,支持集中化的密钥存储和动态凭据生成功能。Vault 的主要功能包括:
- 密钥存储:使用加密方法保护敏感数据和密钥。
- 动态凭据:能够为不同的应用生成和轮转独立的密钥。
- 访问控制:基于策略管理,确保只有授权应用可以访问密钥。
通过 Spring Vault 将HashiCorp Vault无缝集成到 Spring 应用中,用于管理 Spring Authorization Server 授权服务的 JWT 密钥。
安装 Vault
本示例介绍使用docker安装Vault,所以需要你准备docker环境或者通过Vault官网介绍进行二进制安装。
请勿在生产环境使用下述简单的安装步骤,你应该设置访问控制策略。
1. 拉取 Vault 镜像
首先,拉取指定版本的 Vault 镜像(1.13.3):
docker pull vault:1.13.3
2. 运行 Vault 容器
以开发模式运行 Vault 容器:
docker run --cap-add=IPC_LOCK -d --name=dev-vault vault:1.13.3
3.查看 Vault 启动日志
查看Vault启动日志,获取Root Token:
docker logs -f dev-vault
日志输出示例:
Api Address: http://127.0.0.1:8200
Cgo: disabled
Cluster Address: https://127.0.0.1:8201
Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")
Log Level: info
Mlock: supported: false, enabled: false
Recovery Mode: false
Storage: inmem
Version: Vault v1.13.3
WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
and starts unsealed with a single unseal key. The root token is already
authenticated to the CLI, so you can immediately begin using Vault.
You may need to set the following environment variable:
$ export VAULT_ADDR='http://127.0.0.1:8200'
The unseal key and root token are displayed below in case you want to
seal/unseal the Vault or re-authenticate.
Unseal Key: 1+yv+v5mz+aSCK67X6slL3ECxb4UDL8ujWZU/ONBpn0=
Root Token: s.XmpNPoi9sRhYtdKHaQhkHP6x
Development mode should NOT be used in production installations!
4.配置 Vault 客户端
启动一个新的终端会话并进入容器:
docker exec -it dev-vault /bin/sh
设置 Vault 地址:
export VAULT_ADDR='http://127.0.0.1:8200'
将 Root Token 设置为环境变量:
export VAULT_TOKEN="s.XmpNPoi9sRhYtdKHaQhkHP6x"
5.验证 Vault 服务器是否正在运行 在容器内运行以下命令检查服务器状态:
vault status
如果您遇到如下错误,请检查VAULT_ADDR环境变量配置正确。
Error checking seal status: Get "https://127.0.0.1:8200/v1/sys/seal-status": http: server gave HTTP response to HTTPS client
6. 启用 Transit 引擎
vault secrets enable transit
7. 创建支持签名的密钥
使用 rsa-2048 类型的密钥,以支持签名操作:
vault write -f transit/keys/oauth2 type="rsa-2048"
将 Spring Authorization Server 配置为使用 Vault 中的密钥
在 Spring Authorization Server 项目中,修改 application.yml
配置文件,配置Vault服务地址,注意token为Vault启动日志中的Root Token。
spring:
application:
name: authorization-service
cloud:
vault:
scheme: http
uri: http://127.0.0.1:8200
authentication: token
token: ${VAULT_TOKEN}
fail-fast: true
kv:
enabled: true
backend: transit
接下来我们需要自定义VaultJwtEncoder
,使用Vault生成JWT签名。
public final class VaultJwtEncoder implements JwtEncoder {
private final VaultOperations vaultOperations;
private static final JwsHeader DEFAULT_JWS_HEADER = JwsHeader.with(SignatureAlgorithm.RS256).type(JOSEObjectType.JWT.getType()).build();
private String key = "oauth2"; //key需要和vault创建签名密钥key保持一致
public VaultJwtEncoder(VaultOperations vaultOperations) {
Assert.notNull(vaultOperations, "vaultOperations cannot be null");
this.vaultOperations = vaultOperations;
}
@SneakyThrows
@Override
public Jwt encode(JwtEncoderParameters parameters) throws JwtEncodingException {
JwsHeader headers = parameters.getJwsHeader();
if (headers == null) {
headers = DEFAULT_JWS_HEADER;
}
JWSHeader jwsHeader = convert(headers);
JwtClaimsSet claims = parameters.getClaims();
JWTClaimsSet jwtClaimsSet = convert(claims);
JWSObject jwsObject = new JWSObject(jwsHeader, new Payload(jwtClaimsSet.toJSONObject()));
// Sign the JWS object
String signingInput = new String(jwsObject.getSigningInput());
Plaintext plaintext = Plaintext.of(signingInput).with(VaultTransitContext.builder().build());
String signature = vaultOperations.opsForTransit().sign(key, plaintext).getSignature();
// Attach the signature to the JWS object
Base64URL signatureBase64URL = Base64URL.from(signature.startsWith("vault:v1:") ? signature.substring(9) : signature);
jwsObject = new JWSObject(jwsHeader.toBase64URL(), new Payload(jwtClaimsSet.toJSONObject()), signatureBase64URL);
// Serialize JWS to compact format
String jws = jwsObject.serialize();
return new Jwt(jws, claims.getIssuedAt(), claims.getExpiresAt(), headers.getHeaders(), claims.getClaims());
}
...
}
其他Spring Authorization Server授权服务配置和之前文章一致,这里不再赘述,具体配置可以通过文末链接查看源码,这里我们只讲述与文章相关的核心代码逻辑。
资源服务配置Vault进行验证JWT签名
在我们使用Vault在授权服务生成JWT后,资源服务作为JWT的使用者,同样需要验签逻辑进行验证签名,允许客户端请求通过。
通过自定义VaultJwtDecoder
解析JWT的签名字符串,由Vault验证签名是否有效,当签名有效解析JWT其他部分,当签名被篡改,抛出异常终止操作。
public class VaultJwtDecoder implements JwtDecoder {
private String key = "oauth2";
private OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefault();
private Converter<Map<String, Object>, Map<String, Object>> claimSetConverter = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());
private final VaultOperations vaultOperations;
public VaultJwtDecoder(VaultOperations vaultOperations) {
Assert.notNull(vaultOperations, "vaultOperations cannot be null");
this.vaultOperations = vaultOperations;
}
public void setJwtValidator(OAuth2TokenValidator<Jwt> jwtValidator) {
Assert.notNull(jwtValidator, "jwtValidator cannot be null");
this.jwtValidator = jwtValidator;
}
@SneakyThrows
@Override
public Jwt decode(String token) throws JwtException {
SignedJWT jwt = this.parse(token);
Jwt createdJwt = this.createJwt(token, jwt);
return this.validateJwt(createdJwt);
}
private SignedJWT parse(String token) {
try {
return SignedJWT.parse(token);
} catch (Exception e) {
log.trace("Failed to parse token", e);
throw new BadJwtException(String.format("An error occurred while attempting to decode the Jwt: %s", e.getMessage()), e);
}
}
private Jwt createJwt(String token, SignedJWT parsedJwt) {
try {
// Verify signature using Vault
String signingInput = new String(parsedJwt.getSigningInput());
String signature = parsedJwt.getSignature().toString();
Plaintext plaintext = Plaintext.of(signingInput).with(VaultTransitContext.builder().build());
boolean isValid = vaultOperations.opsForTransit().verify(key, plaintext, Signature.of("vault:v1:" + signature));
if (!isValid) {
throw new JOSEException("Token signature is not valid");
}
JWTClaimsSet jwtClaimsSet = parsedJwt.getJWTClaimsSet();
Map<String, Object> headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject());
Map<String, Object> claims = this.claimSetConverter.convert(jwtClaimsSet.getClaims());
return Jwt.withTokenValue(token).headers((h) -> {
h.putAll(headers);
}).claims((c) -> {
c.putAll(claims);
}).build();
} catch (JOSEException e) {
log.trace("Failed to process JWT", e);
throw new JwtException(String.format("An error occurred while attempting to decode the Jwt: %s", e.getMessage()), e);
} catch (Exception e) {
log.trace("Failed to process JWT", e);
if (e.getCause() instanceof ParseException) {
throw new BadJwtException(String.format("An error occurred while attempting to decode the Jwt: %s", "Malformed payload"), e);
} else {
throw new BadJwtException(String.format("An error occurred while attempting to decode the Jwt: %s", e.getMessage()), e);
}
}
}
...
}
验证 JWT 签名和解密过程
通过 Vault 的密钥进行JWT签名和验证,确保所有的 JWT 都是通过 Vault 保护的密钥生成的。我们通过完整的OAuth2.0授权码流程来验证这一过程。
浏览器访问:http://127.0.0.1:8070/client/test
结论
通过将 Vault 集成到 Spring Authorization Server 中,我们可以安全地管理 JWT 密钥,实现了密钥的集中存储和访问控制。通过本文的介绍,希望你可以在自己的应用中尝试这一方法,为用户提供更安全的身份认证服务。
与往常一样,本文中使用的源代码可在 GitHub 上获得。