Skip to content

Http响应加密 ​

一、背景与概述 ​

在一些对数据安全要求较高的场景中,我们需要对服务端返回给客户端的 HTTP 响应体进行加密,以防止传输过程中的明文泄露。tio-boot 在底层已内置了对 HTTP 响应数据的加密支持:

  • Controller 层:如果控制器直接返回对象或字符串,tio-boot 会自动调用配置的加解密器(TioEncryptor)对响应体进行加密。
  • Handler 层:对于底层 HttpRequestHandler,需手动在写入响应体时调用加密方法。

本文档将帮助快速在项目中启用并定制 AES/CBC/PKCS5Padding 算法的 HTTP 加密。


二、快速配置 ​

1. 添加配置类 ​

在项目的任意配置包下新建一个 @AConfiguration 注解的启动配置类,将自定义的加密服务注册给 tio-boot:

java
import nexus.io.annotation.AConfiguration;
import nexus.io.annotation.Initialization;
import nexus.io.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. 自定义加解密实现 ​

实现 nexus.io.tio.boot.encrypt.TioEncryptor 接口,示例使用 AES-128/CBC/PKCS5Padding,并在密文前拼接随机 IV。

java
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import nexus.io.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),则需在生成响应体时自行调用加密器:

java
// 获取当前配置的加密器
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)。

四、客户端解密示例 ​

客户端收到响应后,需要:

  1. 从响应体中取出前 16 字节 IV;
  2. 使用与服务端一致的 AES-128 Key 与 IV 对剩余字节进行解密;
  3. 将解密后的 JSON 字符串反序列化为对象。

示例(JavaScript + CryptoJS)

js
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);

五、注意事项 ​

  1. Key 管理:务必妥善保管 HEX_KEY,生产环境建议从环境变量或安全配置中心读取,不要硬编码在代码中。
  2. 性能开销:AES/CBC 加密会带来一定 CPU 开销,如高并发场景需做好性能测试。
  3. 兼容性:确保客户端的解密库与服务端算法、填充方式(PKCS5Padding)保持一致。
  4. 错误处理:解密失败时应返回明确的错误码,避免客户端出现难以定位的问题。

通过上述步骤,即可在 tio-boot 项目中快速启用并定制 HTTP 返回数据的 AES 加密,满足对传输安全的需求。

MIT Licensed