详细介绍:【微服务】SpringBoot 整合轻量级安全框架JWE 项目实战详解
2025-11-20 16:53 tlnshuju 阅读(25) 评论(0) 收藏 举报目录
2.6.2 JWE(JSON Web Encryption)优势
一、前言
这些年随着互联网技术的飞速发展,人们对于互联网产品在使用过程中的安全性也提出了更高的要求。在微服务开发过程中,几乎所有的需要上线的项目都会在安全方面进行基本的架构和设计,比如大家熟悉的登录认证,仅这一项就有种类繁多的操作方式,比如账户密码登录,手机验证码登录,有的甚至还需要结合三方认证服务进行安全校验,无一不说明对安全的重视程度。在springboot项目中,对安全的防护首先就是如何保护服务端的接口,经过多年的发展,也形成了比较多的解决思路,也产生了比较多的安全认证框架。本文以java生态下使用比较广泛并且比较轻量级的安全框架JWE为例进行详细的说明。
二、JWE 与JWT 介绍
2.1 什么是 JWE
JWE(JSON Web Encryption)是一种基于JSON的数据加密标准,属于JOSE(Javascript Object Signing and Encryption)套件的一部分。它定义了如何对JSON数据进行加密和解密,确保数据的机密性和完整性。官网:https://www.jwt.io/

2.2 JWE 与 JWT 的关系
说到JWE,很难不将其与JWT进行对比,具体来说,两者主要有下面的区别:
JWT (JSON Web Token): 主要用于身份验证和信息交换,可以签名但不加密
JWE (JSON Web Encryption): 对JWT或其他数据进行加密,保护数据机密性
常见组合: JWT + JWE = 安全的加密令牌
JWT简称 JSON Web Token,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输,在数据传输过程中还可以完成数据加密,签名等相关处理。
而JSON Web 加密 (jwe) 是 rfc 7516 定义的标准,它使用基于 json 的数据结构表示加密内容。它允许您加密任意有效负载以确保机密性和完整性(如果需要)。此加密内容可以包括任何类型的数据,例如敏感的用户信息、安全令牌甚至文件。

2.3 JWE 主要特点
jwe 广泛用于 web 应用程序和 api,以安全地传输敏感数据,例如令牌、用户信息和财务详细信息。它确保信息即使被拦截也无法被未经授权的实体读取。加密的有效负载只能由拥有正确解密密钥的预期接收者解密和使用。
其主要特点如下:
保密性:jwe 的首要目标是确保内容的机密性。
完整性:保证数据在传输过程中不被篡改。
互操作性:jwe 与不同的加密算法和环境兼容。
紧凑性:jwe 提供了一种紧凑的表示形式,易于通过 http 传输。
2.4 JWE 数据结构
为了深入学习和掌握JWE,结合下面给出JWE的完整数据结构和图示进行理解,具体来说,一个JWE Token由5个部分组成,用点号分隔,如下:
Base64Url(Header).Base64Url(EncryptedKey).Base64Url(IV).Base64Url(Ciphertext).Base64Url(Tag)
下面对参数的完整结构做补充说明

1)JWE Header
该参数体主要包含加密算法和参数:
{
"alg": "RSA-OAEP-256", // 密钥加密算法
"enc": "A128GCM", // 内容加密算法
"typ": "JWT", // 类型
"cty": "JWT" // 内容类型
}
2)Encrypted Key
使用Header中指定的算法加密的对称密钥
3)Initialization Vector (IV)
加密算法的初始化向量
4)Ciphertext
实际加密的数据
5)Authentication Tag
完整性验证标签
2.5 JWE 中常用的加密算法
在使用JWE对数据进行加密时,其SDK提供了丰富的加密算法可供开发者选择,下面列举几种JWE常用的加密算法:
密钥加密算法 (alg)
RSA系列: RSA-OAEP, RSA-OAEP-256, RSA1_5
AES密钥包装: A128KW, A192KW, A256KW
直接加密: dir
密钥协商: ECDH-ES, ECDH-ES+A128KW
内容加密算法 (enc)
AES GCM: A128GCM, A192GCM, A256GCM
AES CBC: A128CBC-HS256, A192CBC-HS384, A256CBC-HS512
2.6 JWE 对比JWT优势
为什么要选择JWE呢?JWE 和 JWT 并不是相互替代的关系,而是解决不同问题的互补技术。从使用经验来说,更多的还是考虑到安全问题,简单来说:
JWT(JSON Web Token) 的核心是签名,确保数据不被篡改,但数据本身是明文的。
JWE(JSON Web Encryption) 的核心是加密,确保数据的机密性,内容对未经授权的一方完全不可读。
因此,讨论 JWE 的优势,实际上可以说是在讨论“加密的令牌”相对于“签名的令牌”在特定场景下的优势。
2.6.1 JWT(通常指JWS)局限性
通常所说的 JWT,绝大多数情况下指的是经过签名(JWS,JSON Web Signature)的令牌。它的结构是 Header.Payload.Signature。
Payload 是 Base64Url 编码的,不是加密的。任何人都可以轻松地将 Payload 部分解码,看到里面的所有声明(Claims),如 email, userId, role 等。Payload 的基本结构如下:
// Header
{
"alg": "HS256",
"typ": "JWT"
}
// Payload (明文,仅Base64编码)
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
基于上面的分析不难发现,JWT(JWS)的主要问题如下:
敏感信息泄露:如果令牌中包含邮箱、手机号、权限等敏感信息,这些信息在传输和存储过程中都是暴露的。如果令牌被中间人截获或在客户端(如浏览器 LocalStorage)被恶意脚本读取,敏感信息就泄露了。
不适合在不可信方传递:JWT 经常被发给客户端(如浏览器、移动App)。客户端是一个不可完全信任的环境。你不应该让客户端看到任何它不该看的信息。
2.6.2 JWE(JSON Web Encryption)优势
JWE 的结构是 Header.EncryptedKey.IV.Ciphertext.AuthenticationTag,它将整个有效载荷(Payload)进行了加密。这样做的优势如下:
机密性
这是最核心的优势。 JWE 确保了令牌内容只有持有正确解密密钥的接收方才能读取。
解决了敏感信息泄露问题:即使令牌被截获,攻击者也无法得知里面的内容。这对于遵守数据隐私法规(如GDPR、HIPAA)至关重要。
同时提供机密性和完整性
JWE 不仅加密数据,还通过 Authentication Tag 提供了完整性保护,确保加密后的数据在传输过程中没有被篡改。它同时做到了 JWS(完整性)和加密(机密性)的工作。
适用于包含敏感数据的场景
身份信息:如完整的用户档案、地址、社保号等。
授权码:在 OAuth 2.0 流程中,授权码本身就是一个敏感凭证,使用 JWE 格式传输更为安全。
客户端凭证:当后端服务需要向客户端传递一些它需要使用但不应理解的“黑盒”数据时。
服务间通信:在微服务架构中,如果一个令牌需要穿过多个中间服务才能到达目标服务,而你又不想让中间服务看到令牌内容,JWE 是理想选择。
灵活性
JWE 支持多种加密算法(如 RSA-OAEP, A128GCM等),允许开发者根据安全需求和性能考虑进行选择。
通过下面这张表的对比能看的更清楚:
| 特性 | JWT(通常指 JWS) | JWE |
| 核心目标 | 数据完整性 和 认证 | 数据机密性 和 完整性 |
| Payload 状态 | 明文,仅 Base64Url 编码 | 加密的密文 |
| 可读性 | 任何人可解码并读取 Payload | 只有持有密钥的接收方可解密并读取 |
| 主要风险 | 敏感信息泄露 | 密钥管理复杂(如果密钥泄露,则安全性丧失) |
| 典型结构 | Header.Payload.Signature | Header.EncryptedKey.IV.Ciphertext.AuthenticationTag |
| 适用场景 | 会话管理、无状态认证、公开信息传递 | 传输敏感数据、保护授权码、服务间安全通信 |
2.6.3 JWE与JWT 最佳实践探索
事实上,JWE 和 JWS(JWT)可以被结合使用,以实现最佳的安全效果。这被称为“签名然后加密”或“加密然后签名”模式。一个常见的模式是:
内部创建一个 JWS:首先,认证服务器创建一个包含用户声明的 JWS,以确保数据的完整性。
对外发布一个 JWE:然后,将这个 JWS 整个作为 payload,用 JWE 进行加密。
结果:客户端得到的是一个 JWE。它自己无法解密,当它把这个 JWE 发给资源服务器时,资源服务器先解密,得到里面的 JWS,然后再验证 JWS 的签名。
这种方式既保证了令牌内容不被客户端窥探(机密性),又保证了令牌是由可信的认证服务器颁发的(完整性和认证)。
小结:
JWE 相对于 JWT优势在于提供了更强大的安全性:
如果你需要在不安全的信道(如互联网)上传输令牌,或者需要将令牌存储在不可信的客户端,并且令牌内包含了任何敏感信息时,就应该优先考虑使用 JWE。
如果你的令牌中只包含一些不敏感的且可以公开的信息,比如用户ID,并且安全依赖于签名验证和短暂的过期时间,那么使用JWT就够了。
选择哪一个,完全取决于你的应用场景和对数据机密性的要求。在安全要求极高的系统中,“JWS嵌套在JWE内”是黄金标准。
2.7 JWE 优缺点
尽管JWE能够带来更多的安全性,但是实际选择的时候也需要进行取舍,下面列举了JWE的优缺点,方便技术选型的使用做完参考。
1)优点
保密性:提供端到端加密,确保数据隐私。
互操作性:跨不同系统和平台兼容。
完整性和安全性:确保数据免遭篡改。
支持多个收件人:允许使用不同的密钥将数据加密到多个收件人。
2)缺点
复杂性高:加密和解密的过程可能很复杂且容易出错。
性能开销大:加密/解密过程会增加计算开销,选择的加密算法越复杂,加解密时开销越高。
更大的有效负载大小:由于加密元数据,jwe 有效负载比纯数据或 jwt 更大。
三、基于JWT 加解密项目整合
为了在后续使用JWE过程中有更深的理解,并与JWT形成比较清晰的理解和对比,首先通过一个JWT的案例来重温一下JWT的基本功能使用。
3.1 JWT 基本使用
3.1.1 导入基本的依赖
在pom文件中导入基本的JWT依赖
org.springframework.boot
spring-boot-starter-web
io.jsonwebtoken
jjwt
0.9.1
javax.xml.bind
jaxb-api
2.3.1
org.projectlombok
lombok
3.1.2 添加一个JWT工具类
自定义一个JWT工具类,参考实际项目中的一个实际业务场景,分别提供生成token,解析token,验证token的有效性这几个方法,参考下面的代码:
package com.congge.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtil {
// 密钥
private final String SECRET_KEY = "mySecretKey123456789012345678901234567890";
// Token有效期(7天)
private final long EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 7;
/**
* 生成Token
*/
public String generateToken(String username) {
Map claims = new HashMap<>();
return createToken(claims, username);
}
private String createToken(Map claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
/**
* 从Token中提取用户名
*/
public String extractUsername(String token) {
return extractAllClaims(token).getSubject();
}
/**
* 提取Claims
*/
private Claims extractAllClaims(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
/**
* 验证Token是否有效
*/
public Boolean validateToken(String token) {
try {
return !isTokenExpired(token);
} catch (Exception e) {
return false;
}
}
/**
* 检查Token是否过期
*/
private Boolean isTokenExpired(String token) {
return extractAllClaims(token).getExpiration().before(new Date());
}
/**
* 从请求头中提取Token
*/
public String extractTokenFromHeader(String authorizationHeader) {
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
return authorizationHeader.substring(7);
}
return null;
}
}
3.1.3 提供一个自定义controller
为了方便测试看效果,提供2个测试的接口,参考下面的代码
package com.congge.web;
import com.congge.entity.User;
import com.congge.jwt.JwtUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
// 模拟用户数据
private Map userDatabase = new HashMap<>();
public AuthController() {
// 初始化模拟用户
userDatabase.put("admin", "admin123");
userDatabase.put("user", "user123");
}
@PostMapping("/login")
public ResponseEntity login(@RequestBody User loginUser) {
String username = loginUser.getUsername();
String password = loginUser.getPassword();
// 验证用户名和密码
if (!userDatabase.containsKey(username) || !userDatabase.get(username).equals(password)) {
Map response = new HashMap<>();
response.put("error", "用户名或密码错误");
return ResponseEntity.badRequest().body(response);
}
// 生成Token
String token = jwtUtil.generateToken(username);
Map response = new HashMap<>();
response.put("token", token);
response.put("username", username);
response.put("message", "登录成功");
return ResponseEntity.ok(response);
}
@Resource
private HttpServletRequest request;
@GetMapping("/verify")
public ResponseEntity verifyToken() {
String authHeader = request.getHeader("Authorization");
String token = jwtUtil.extractTokenFromHeader(authHeader);
if (token == null) {
return ResponseEntity.badRequest().body("缺少Token");
}
if (!jwtUtil.validateToken(token)) {
return ResponseEntity.badRequest().body("Token无效或已过期");
}
String username = jwtUtil.extractUsername(token);
Map response = new HashMap<>();
response.put("username", username);
response.put("message", "Token验证成功");
response.put("status", "valid");
return ResponseEntity.ok(response);
}
}
3.2 效果测试
启动工程后,分别测试一下2个接口。
1)模拟登录,生成token
在接口工具中,调用下第一个生成token接口

2)验证token是否有效
在接口工具中,验证token是否有效

四、基于JWE 加解密项目整合
接下来通过实际案例演示在springboot项目中如何整合JWE使用。
4.1 导入基本依赖
在pom文件中导入下面的JWE依赖
com.nimbusds
nimbus-jose-jwt
9.37.3
4.2 提供JWE加解密工具类
自定义给一个JWE的工具类,在工具类中提供常用的加解密方法,仍然以上述的关于token的生成和解析为业务背景,提供token的创建,token解析,token校验是否有效几个方法,参考下面的代码
package com.congge.jwe;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import com.nimbusds.jwt.EncryptedJWT;
import com.nimbusds.jwt.JWTClaimsSet;
import java.text.ParseException;
import java.util.Date;
import java.util.Map;
public class JweUtil {
private final RSAKey rsaKey;
private final JWEAlgorithm algorithm;
private final EncryptionMethod encryptionMethod;
public JweUtil() throws JOSEException {
// 生成RSA密钥对
this.rsaKey = new RSAKeyGenerator(2048)
.keyID("123")
.generate();
this.algorithm = JWEAlgorithm.RSA_OAEP_256;
this.encryptionMethod = EncryptionMethod.A128GCM;
}
public JweUtil(RSAKey rsaKey) {
this.rsaKey = rsaKey;
this.algorithm = JWEAlgorithm.RSA_OAEP_256;
this.encryptionMethod = EncryptionMethod.A128GCM;
}
/**
* 创建JWE Token
*/
public String createJweToken(String subject, Map claims,
long expirationMinutes) throws JOSEException {
// 创建JWT声明
JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder()
.subject(subject)
.issueTime(new Date())
.expirationTime(new Date(System.currentTimeMillis() + expirationMinutes * 60 * 1000));
// 添加自定义声明
if (claims != null) {
claims.forEach(claimsBuilder::claim);
}
JWTClaimsSet jwtClaims = claimsBuilder.build();
// 创建JWE头
JWEHeader header = new JWEHeader.Builder(algorithm, encryptionMethod)
.contentType("JWT") // 表明加密的内容是JWT
.keyID(rsaKey.getKeyID())
.build();
// 创建加密的JWT
EncryptedJWT encryptedJWT = new EncryptedJWT(header, jwtClaims);
// 使用RSA公钥加密
RSAEncrypter encrypter = new RSAEncrypter(rsaKey.toRSAPublicKey());
encryptedJWT.encrypt(encrypter);
return encryptedJWT.serialize();
}
/**
* 解析JWE Token
*/
public JWTClaimsSet parseJweToken(String jweToken)
throws ParseException, JOSEException {
// 解析JWE Token
EncryptedJWT encryptedJWT = EncryptedJWT.parse(jweToken);
// 使用RSA私钥解密
RSADecrypter decrypter = new RSADecrypter(rsaKey.toRSAPrivateKey());
encryptedJWT.decrypt(decrypter);
return encryptedJWT.getJWTClaimsSet();
}
/**
* 验证JWE Token是否有效
*/
public boolean validateJweToken(String jweToken) {
try {
JWTClaimsSet claims = parseJweToken(jweToken);
return claims.getExpirationTime().after(new Date());
} catch (Exception e) {
return false;
}
}
/**
* 刷新JWE Token
*/
public String refreshJweToken(String jweToken, long newExpirationMinutes)
throws ParseException, JOSEException {
JWTClaimsSet oldClaims = parseJweToken(jweToken);
// 创建新的声明集,保留除时间外的所有声明
JWTClaimsSet.Builder newClaimsBuilder = new JWTClaimsSet.Builder()
.subject(oldClaims.getSubject())
.issueTime(new Date())
.expirationTime(new Date(System.currentTimeMillis() + newExpirationMinutes * 60 * 1000));
// 复制自定义声明
oldClaims.getClaims().forEach((key, value) -> {
if (!"iat".equals(key) && !"exp".equals(key) && !"nbf".equals(key)) {
newClaimsBuilder.claim(key, value);
}
});
return createJweToken(oldClaims.getSubject(),
newClaimsBuilder.build().getClaims(), newExpirationMinutes);
}
public RSAKey getRsaKey() {
return rsaKey;
}
}
通过创建token的方法不难看出,对请求的header以及claim进行了加密,这样的话,万一说token被泄漏了,有人拿到了,仍然无法直接进行破解,因为私钥存储在服务端,理论上讲,只要私钥没有泄漏,加密算法没有泄漏,这个token就是安全的。
4.3 提供JWE加解密测试用例
为了验证效果,提供一个测试类(也可以通过编写接口进行验证),对上述的工具类中的核心方法进行效果验证,参考下面的代码
package com.congge.jwe;
import com.nimbusds.jwt.JWTClaimsSet;
import java.util.Map;
public class JweExampleTest {
public static void main(String[] args) {
try {
// 1. 创建JWE工具实例
JweUtil jweUtil = new JweUtil();
// 2. 准备声明数据
Map claims = Map.of(
"userId", 12345,
"username", "john_may",
"roles", new String[]{"USER", "ADMIN"},
"email", "john_may@example.com"
);
// 3. 创建JWE Token
String jweToken = jweUtil.createJweToken("john_may", claims, 60); // 60分钟过期
System.out.println("JWE Token: " + jweToken);
// 4. 解析和验证JWE Token
JWTClaimsSet parsedClaims = jweUtil.parseJweToken(jweToken);
System.out.println("Subject: " + parsedClaims.getSubject());
System.out.println("Username: " + parsedClaims.getClaim("username"));
System.out.println("Roles:" + parsedClaims.getClaim("roles"));
// 5. 验证Token有效性
boolean isValid = jweUtil.validateJweToken(jweToken);
System.out.println("Token is valid: " + isValid);
// 6. 刷新Token
String refreshedToken = jweUtil.refreshJweToken(jweToken, 120);
System.out.println("Refreshed Token: " + refreshedToken);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行上面的程序,观察控制台的输出效果,可以看到,通过这种方式得到的加密字符串更加复杂,其结构与上述我们分解其内部结构一致。

4.4 整合SpringBoot 常用的优化思路
通常来说,针对JWE或者JWT这种类型的token认证,行业中比较通用的做法可以参考下面的完整思路:
编写JWE(JWT)工具类,结合实际业务需求,比如密钥,claims承载的信息,安全等级等信息编写相应的工具方法,至少包括:token生成,token解密得到claims的信息,token验证是否有效,token刷新;
编写全局的过滤器(拦截器),如果有gateway这样的网关也可以,在前端进入服务端接口之前,先在拦截器(过滤器/网关)中进行拦截,验证token的有效性,token有效才能走到真正的接口进行逻辑处理;
程序应该有针对token过期时间的自动续约(自动刷新token)的处理,对于那些在页面上一直在操作的行为,程序需要对这类token进行自动的续约,确保比较好的用户体验;
五、写在最后
本文通过实际的案例演示了如何使用JWT与JWE的完整过程,在如今的互联网时代背景下,人们对于安全的诉求越来越高,选择安全度更高的技术一直是所有的应用开发者在持续追求的,希望对看到的同学有用哦,本篇到此结束,感谢观看。
浙公网安备 33010602011771号