Http响应加密
一、背景与概述
在一些对数据安全要求较高的场景中,我们需要对服务端返回给客户端的 HTTP 响应体进行加密,以防止传输过程中的明文泄露。tio-boot 在底层已内置了对 HTTP 响应数据的加密支持:
- Controller 层:如果控制器直接返回对象或字符串,tio-boot 会自动调用配置的加解密器(
TioEncryptor
)对响应体进行加密。 - Handler 层:对于底层
HttpRequestHandler
,需手动在写入响应体时调用加密方法。
本文档将帮助你快速在项目中启用并定制 AES/CBC/PKCS5Padding 算法的 HTTP 加密。
二、快速配置
1. 添加配置类
在项目的任意配置包下新建一个 @AConfiguration
注解的启动配置类,将自定义的加密服务注册给 tio-boot:
import com.litongjava.annotation.AConfiguration;
import com.litongjava.annotation.Initialization;
import com.litongjava.tio.boot.server.TioBootServer;
@AConfiguration
public class EncryptConfig {
@Initialization
public void config() {
try {
// 将自定义的 AddrEncryptService 注入到 tio-boot 中
TioBootServer.me().setTioEncryptor(new AddrEncryptService());
} catch (Exception e) {
e.printStackTrace();
}
}
}
- @AConfiguration:标记该类为配置类,tio-boot 启动时会扫描并执行。
- @Initialization:标记方法在容器初始化完成后执行,用于注册加密器。
2. 自定义加解密实现
实现 com.litongjava.tio.boot.encrypt.TioEncryptor
接口,示例使用 AES-128/CBC/PKCS5Padding,并在密文前拼接随机 IV。
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import com.litongjava.tio.boot.encrypt.TioEncryptor;
/**
* 使用 AES/CBC/PKCS5Padding 加解密,
* - 固定 16 字节 AES-128 Key(由 32 位十六进制字符串派生)
* - 每次加密前随机生成 16 字节 IV
* - 最终密文 = IV + AES(明文)
*/
public class AddrEncryptService implements TioEncryptor {
/** 32 位十六进制字符串,代表 16 字节 AES-128 Key */
private static final String HEX_KEY = "xxx";
/** 将十六进制字符串转换为字节数组 */
private static byte[] hexStringToBytes(String hex) {
int len = hex.length();
byte[] result = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
result[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
}
return result;
}
/** 固定的 AES Key Bytes */
private static final byte[] AES_KEY_BYTES = hexStringToBytes(HEX_KEY);
@Override
public byte[] encrypt(byte[] sources) {
try {
// 1. 随机生成 16 字节 IV
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
// 2. 构造 AES Key
SecretKeySpec keySpec = new SecretKeySpec(AES_KEY_BYTES, "AES");
// 3. 执行 AES/CBC/PKCS5Padding 加密
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
byte[] cipherBytes = cipher.doFinal(sources);
// 4. 拼接 IV 与密文
ByteBuffer buffer = ByteBuffer.allocate(iv.length + cipherBytes.length);
buffer.put(iv);
buffer.put(cipherBytes);
return buffer.array();
} catch (Exception e) {
throw new RuntimeException("AES 加密失败", e);
}
}
@Override
public byte[] decrypt(byte[] sources) {
try {
// 将输入拆分为 IV 和实际密文
ByteBuffer buffer = ByteBuffer.wrap(sources);
byte[] iv = new byte[16];
buffer.get(iv);
byte[] cipherBytes = new byte[buffer.remaining()];
buffer.get(cipherBytes);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
SecretKeySpec keySpec = new SecretKeySpec(AES_KEY_BYTES, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
return cipher.doFinal(cipherBytes);
} catch (Exception e) {
throw new RuntimeException("AES 解密失败", e);
}
}
}
三、在 Handler 层手动加密
如果你直接使用低级别的 Handler(而非 Controller),则需在生成响应体时自行调用加密器:
// 获取当前配置的加密器
TioEncryptor tioEncryptor = TioBootServer.me().getTioEncryptor();
// 将要返回的对象转换为 JSON 字节
String charset = request.getHttpConfig().getCharset();
byte[] bytes = Json.getJson().toJsonBytes(kvs);
// 执行加密(若未配置 加密器,则 tioEncryptor 为 null,直接返回明文)
if (tioEncryptor != null) {
bytes = tioEncryptor.encrypt(bytes);
}
// 设置响应体与 Content-Type
response.setBody(bytes);
response.setContentType(MimeTypeUtils.getJson(charset));
- Json 序列化:你可以替换为任意 JSON 工具,关键是拿到
byte[]
。 - MimeType:确保客户端能正确解析(例如
application/json; charset=UTF-8
)。
四、客户端解密示例
客户端收到响应后,需要:
- 从响应体中取出前 16 字节 IV;
- 使用与你服务端一致的 AES-128 Key 与 IV 对剩余字节进行解密;
- 将解密后的 JSON 字符串反序列化为对象。
示例(JavaScript + CryptoJS)
import CryptoJS from "crypto-js"; // 假设从响应中读取到 base64 编码后的 ciphertext const base64 = response.data; const data = CryptoJS.enc.Base64.parse(base64); // 分离 IV 与密文 const iv = CryptoJS.lib.WordArray.create(data.words.slice(0, 4)); const cipherWords = data.words.slice(4); const cipherParams = CryptoJS.lib.CipherParams.create({ ciphertext: CryptoJS.lib.WordArray.create(cipherWords) }); // 固定的 key(同后端 HEX_KEY 派生) const key = CryptoJS.enc.Hex.parse("xxx"); // 解密 const decrypted = CryptoJS.AES.decrypt(cipherParams, key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); const jsonStr = decrypted.toString(CryptoJS.enc.Utf8); const obj = JSON.parse(jsonStr);
五、注意事项
- Key 管理:务必妥善保管
HEX_KEY
,生产环境建议从环境变量或安全配置中心读取,不要硬编码在代码中。 - 性能开销:AES/CBC 加密会带来一定 CPU 开销,如高并发场景需做好性能测试。
- 兼容性:确保客户端的解密库与服务端算法、填充方式(
PKCS5Padding
)保持一致。 - 错误处理:解密失败时应返回明确的错误码,避免客户端出现难以定位的问题。
通过上述步骤,你即可在 tio-boot 项目中快速启用并定制 HTTP 返回数据的 AES 加密,满足对传输安全的需求。