采用AES和RSA混合模式传输敏感数据

采用AES和RSA混合模式传输敏感数据

简介

什么是AES算法

AES是对称加密算法,其安全性也经得住考验,主要优点是加解密效率高,适合大规模数据的加密。

什么是RSA算法

RSA是非对称加密算法,主要优点是安全性较高。但是,非对称加密算法的加解密效率较低,而且加密数据长度受到限制。

两者的区别

  1. AES加密算法:

- 种类:AES是对称加密算法,数据发送者和接收者使用同一个密钥进行加密和解密。

- 优点:AES加密和解密效率非常高,能够快速地处理大量数据,这使得AES非常适合在需要对大量数据进行加密的场合使用。

- 缺点:AES的主要安全问题是密钥的管理和分发。如果密钥在被发送给接收者的过程中被截获,那么数据的安全性将无法得到保证。

  1. RSA加密算法:

- 种类:RSA是非对称加密算法,它使用一对秘钥,一个公开(公钥),一个保密(私钥)。公钥可广泛发给每个请求者,私钥必须保密。收信人用请求者的公钥加密消息,请求者用自己的私钥解密消息。

- 优点:RSA的主要优点在于其非对称性,使得密钥的分发更为安全。只要私钥没有泄露,即使公钥和加密后的数据被截获,也无法得到原始数据。

- 缺点:与AES相比,RSA的加密和解密速度较慢。另外,RSA不适合用于加密大量数据,而是主要用于加密小的密钥和散列。

由于这两种算法各自的优点和缺点,它们经常被组合使用,以提高整体的安全性和效率。例如,可以用RSA算法加密AES的密钥,然后用AES加密实际的数据。

前后端加解密流程图

代码实现

环境:

jdk17+

node20.12.2 +

typescript5.4.5+

vue3.4.3+

前端工具类:RSAUtil.ts

需要安装依赖库:

pnpm i  -D @types/node-forge node-forge

编写工具类:RSAUtil.ts

import * as forge from "node-forge";

class RSAUtilsForge {
  // 公私钥在设置时就被转换并存储为PublicKey和PrivateKey对象,而非字符串,这样就避免了在每次加解密操作时重复导入密钥。
  private publicKey: forge.pki.rsa.PublicKey | null;

  private privateKey: forge.pki.rsa.PrivateKey | null;

  constructor(publicKeyBase64String?: string, privateKeyBase64String?: string) {
    this.publicKey = publicKeyBase64String
      ? this.importPublicKey(this.toPem(publicKeyBase64String, "public"))
      : null;
    this.privateKey = privateKeyBase64String
      ? this.importPrivateKey(this.toPem(privateKeyBase64String, "private"))
      : null;
  }

  public setPublicKey(publicKeyBase64String: string) {
    this.publicKey = this.importPublicKey(
      this.toPem(publicKeyBase64String, "public")
    );
  }

  public setPrivateKey(privateKeyBase64String: string) {
    this.privateKey = this.importPrivateKey(
      this.toPem(privateKeyBase64String, "private")
    );
  }

  private importPublicKey(pem: string): forge.pki.rsa.PublicKey {
    try {
      return forge.pki.publicKeyFromPem(pem);
    } catch (error) {
      throw new Error("Failed to import public key: " + error.message);
    }
  }

  private importPrivateKey(pem: string): forge.pki.rsa.PrivateKey {
    try {
      return forge.pki.privateKeyFromPem(pem);
    } catch (error) {
      throw new Error("Failed to import private key: " + error.message);
    }
  }

  public encrypt(data: string, isBase64Encode: boolean = false): string {
    if (!this.publicKey) {
      throw new Error("Public key not set");
    }
    const encrypted: string = this.publicKey.encrypt(data, "RSA-OAEP", {
      md: forge.md.sha256.create(),
      mgf1: {
        md: forge.md.sha256.create()
      }
    });

    if (isBase64Encode) {
      return this.encodeBase64(encrypted);
    } else {
      return encrypted;
    }
  }

  public decrypt(data: string): string {
    if (!this.privateKey) {
      throw new Error("Private key not set");
    }
    const decrypted = this.privateKey.decrypt(
      this.isValidBase64(data) ? this.decodeBase64(data) : data,
      "RSA-OAEP",
      {
        md: forge.md.sha256.create(),
        mgf1: {
          md: forge.md.sha256.create()
        }
      }
    );

    return decrypted;
  }

  /**
   * 分段加密算法
   * @description:
   * @param {string} data
   * @param {number} chunkSize 根据使用的算法来确定大小:在这个工具类中最大编码长度是190个字符
   * @return {*} 返回加密后的分段string数组,
   */
  public encryptStringArrayInChunks(
    data: string,
    chunkSize: number = 190
  ): string[] {
    const chunks: string[] = [];
    let offset = 0;

    while (offset < data.length) {
      const chunk = data.substring(
        offset,
        Math.min(offset + chunkSize, data.length)
      );

      const encryptedChunk = this.encrypt(chunk, false);
      chunks.push(encryptedChunk);
      offset += chunkSize;
    }

    return chunks;
  }

  /**
   * @description: 将数据加密后转为base64
   * @param {string} data
   * @param {number} chunkSize
   * @return {*}
   */
  public encryptStringInChunks(
    data: string,
    chunkSize: number = 190,
    isBaseEncode: boolean = true
  ): string {
    const encryptedData = this.encryptStringArrayInChunks(data, chunkSize);
    const result = encryptedData.join("");
    return isBaseEncode ? forge.util.encode64(result) : result;
  }

  /**
   * @description: 解密sting数组 和 encryptStringArrayInChunks 保持一致
   * @param {string} encryptedChunks
   * @return {*}
   */
  public decryptStringArrayInChunks(encryptedChunks: string[]): string {
    let decrypted = "";

    for (let chunk of encryptedChunks) {
      const decryptedChunk = this.decrypt(chunk, false);
      decrypted += decryptedChunk;
    }

    return decrypted;
  }

  /**
   * @description:分段解密方法:data一般是后端返回给前端的密文字符串
   * @param {string} data
   * @param {number} chunkSize 如果算法的长度2048字节的话那么填充后的字符长度就是256,在改算法中填充长度是256
   * @return {*}
   */
  public decryptStringInChunks(data: string, chunkSize: number = 256): string {
    // 先判断是否是base64密文如果是先解密
    if (this.isValidBase64(data)) {
      data = forge.util.decode64(data);
      console.log("data is base64");
    }
    const chunks: string[] = [];
    let offset = 0;

    while (offset < data.length) {
      const chunk = data.substring(
        offset,
        Math.min(offset + chunkSize, data.length)
      );
      const encryptedChunk = this.decrypt(chunk);
      chunks.push(encryptedChunk);
      offset += chunkSize;
    }

    return chunks.join("");
  }

  public encodeBase64(data: string): string {
    return forge.util.encode64(data);
  }

  public decodeBase64(data: string): string {
    if (!this.isValidBase64(data)) {
      throw new Error("Invalid Base64 string");
    }
    return forge.util.decode64(data);
  }

  private isValidBase64(str: string): boolean {
    // Base64字符串应该只包含特定的字符集:
    // 大小写字母、数字以及 '+' 和 '/',末尾可能会有 '=' 用来填充。
    // 此正则表达式验证这些规则,并且对长度有所要求(Base64编码的长度是4的倍数)。
    const base64Regex =
      /^(?:[A-Za-z0-9+/]{4})*?(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
    return base64Regex.test(str);
  }

  private toPem(base64Key: string, type: "public" | "private"): string {
    const matches = base64Key.match(/.{1,64}/g); // Divided into 64 char lines
    if (!matches) {
      throw new Error("Invalid Base64 key format");
    }
    const keyType = type === "public" ? "PUBLIC" : "PRIVATE";
    const formattedKey: string = matches.join("\n");
    return `-----BEGIN ${keyType} KEY-----\n${formattedKey}\n-----END ${keyType} KEY-----`;
  }
}

export default RSAUtilsForge;

// To encrypt

const rsautilForge = new RSAUtilsForge(
  "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"
);
try {
  const data =
    "k8SCMMALxjXUNeNwhn2IUW2sCK5FW6t6ZnUIj3o4WgMkgxHPdZZPcuGcyr2oTCnnVX327J+g8VT1AYNCJD3x+hJ46tGRzx9mWgdM3l0a8K8skXghFE/hqqWk+mvZk8QghZavqf3qim9sgWoqXDKZKS1G7QHYGsTD1zh3KPQ1Px0Gmue7pv5dZCK2MLeCMBj78PcoF9cdNsfSXPs9Ed7QrH6SXAQWyl/aLDm4iDVSzSXhr/LkQwxFFVD";
  ///////////// 第一组测试  /////////////
  const encryptedData = rsautilForge.encryptStringInChunks(data);
  console.log("前端:Encrypted text:", encryptedData);

  // 测试工具类解密
  const decryptedData = rsautilForge.decryptStringInChunks(encryptedData);
  console.log("前端:Decrypted text:", decryptedData);

  // 测试后端的加密密文
  const data1 = "k8SCMMALxjXUNeNwhn2IUW2sCK5FW6t6ZnUIj3o4WgMkgxHPdZZPcuGcyr2oTCnnVX327J+g8VT1AYNCJD3x+hJ46tGRzx9mWgdM3l0a8K8skXghFE/hqqWk+mvZk8QghZavqf3qim9sgWoqXDKZKS1G7QHYGsTD1zh3KPQ1Px0Gmue7pv5dZCK2MLeCMBj78PcoF9cdNsfSXPs9Ed7QrH6SXAQWyl/aLDm4iDVSzSXhr/LkQwxFFVDJLbWg/zbQO/T0ngNxRCBB/Nz3nZUH3XyxkdEHtOg0k0/ROTIxFKFgov+k7aVZ8jK0puMnXjnTRcYUJCQRCjkupiB7VU5GvjRhHsZDS80KJ61eC6g7vQV9zsfsrv8JO5Q7mcOerOp6emXrid2Ww+/PKLgOIKWuTiXtyOAaDUYtygFrxDy0l6DKcnqFh0HTmWmGKXimEwXctUcQaQiOAoPRvUR6hPdaMR66UyULfdSRVFfTjdZBtBmzVUbimrUm6BIUe4CzgGU3/wOpzb/it3pGt+L9e3yVlNI0BVYfWOsiFqzf2jGnY2wFLo3QwE2CXKp/mFFmtvo83KDB5VpM7oT0fCzzhPbtaL5/ktmK8ExzApMKggMWeW6Km56BeBTeXH/5YDbe+5YvwU/5Tu3JnkLkipQv2qz/dwIGWJFt+qUxkL+xx0oMBOI=";
  const decryptedData1 = rsautilForge.decryptStringInChunks(data1);
  console.log("后端 Decrypted text:", decryptedData1);

  ///////////// 第一组测试  /////////////

  const encryptedArrayData = rsautilForge.encryptStringArrayInChunks(data);
  console.log(
    "前端:Encrypted text:",
    rsautilForge.encodeBase64(encryptedArrayData.join(""))
  );

  const descdata = rsautilForge.decryptStringArrayInChunks(encryptedArrayData);
  console.log("前端:Decrypted text:",  descdata);

} catch (error) {
  console.error("Encryption/Decryption error:", error);
}

前端控制台

前端:Encrypted text: lA1+gP6zmKskiS3pRejswG9PPgmy+96Z0656pTEaqd/qeQEthyKcRvTJe9McHuvf9mN9kTBoBp5WgUhI8QN7gAYWlNip3HNq97X+ZcUE28S1kzl+VSplFH+xCAlQFQ1egZLX0U6tOTpj8VPHm67nhFed86O91lB7r4zoe/EraNTLRpbvvREDQiGUAOrRGjX+d/jJtpApjKqYyaiv6zAYbza1kKySvRveEbNms/obiAABHAkKcrSgP/bbl0ebuhQ/XVGZhONU4JmxqQz1hr4Vt8jIWYu8fecIEj1pqstsx6nHeHd9xv6XWi1vbq61E8/MsIaTxc3BTqc4WRpSNHDDECa1qApicGk7nzMVCu8meB7HkrsJBKUiFFavO7Cpz8rHpsOoU5cQ9/Qwaw8MXSLVYqldNirKFkVXjAXGkNf1vXvTz1/I05Bzit+2XQgiJ8A/uFx0h/KQK4LfUOVwCcDB41c177Vs+bxDtJugefUV65iug7Gc1HMcu63VfKT374Pas0AnImfrRBVm8RnUxrsUVLBbkvrhd9zo/Z3ChfgbzQL5LoHGlBQd1WQt8AfR9E4UU5SHrIxeDRJDBsTr5vUhuBwYVlckUMldXCjfVGoSFAustnj4JF87NkyElwLR+vs5drUn+VSttHBZ+J5fXu33VeFHi5JSGNMJ+4WoWIUs2EU=
RSAUtils2.ts:153 data is base64
RSAUtils2.ts:219 前端:Decrypted text: k8SCMMALxjXUNeNwhn2IUW2sCK5FW6t6ZnUIj3o4WgMkgxHPdZZPcuGcyr2oTCnnVX327J+g8VT1AYNCJD3x+hJ46tGRzx9mWgdM3l0a8K8skXghFE/hqqWk+mvZk8QghZavqf3qim9sgWoqXDKZKS1G7QHYGsTD1zh3KPQ1Px0Gmue7pv5dZCK2MLeCMBj78PcoF9cdNsfSXPs9Ed7QrH6SXAQWyl/aLDm4iDVSzSXhr/LkQwxFFVD
RSAUtils2.ts:153 data is base64
RSAUtils2.ts:224 后端 Decrypted text: z8dP0WUUXCHzL8nGCdK1WwUU3iEPYgBvhLt15j1Rs9GYr6bGjbFeg97zwobs88nG8IvXCGcrtuRvxT9Haf6KqDgamMo5DIpUpMNWoXT1Y6jGvjgWZvagcsN4btajU93IvQgborMeXFAQoggsqL04xCfxSnvJjDawPsSJ3qtoWa2bV6N6c8WXuwWZAWYwWSYihrP4jSz0Tg7EDftMkDPzlUYuKoqQ8Rs0lnFFlsD5n0nGvNYKWP4IdoKXcafgp8wP
RSAUtils2.ts:229 前端:Encrypted text: m+5at9Gso+0Eu5Vcyx679lGQffrUOiJTspoQ+dZndu02YeU5B0O6ZLCEkMVPns+uM79NyanifwjtgzkoHj6wxnMC8fNfBjCc04aNbZve0qWrEuWlWKqVklFcXlqB1oQXZqaGbdLrtAqqbuSn+kMRaA82pZMOKqAhjWADY3xHXcYousChtKOB86v2eGytbs0todQk8VI+M2qBZ20nbjhM1hdDsGVycNp3qjYncVCNND0DiHvJqdeCnzoLC8z8By+cOP5PRLO3jyrLzyea7K6Uud+xVLZMK8+whU5pIg9CRTycMGwiIZejTPzptp7CmmlTc6hmiTeNOuU/dsQtdayScjT+OvFgLqgE8C5tnFGKEa3Zr6/UceAuTCN1mxL3nvDnpHKUlMoa5m3nYagv5/wmZoOiDEULFBkZd1irdkYmk5WXqbOrV2seLTS95M6eF+Obm7QuxGiMDWgZ4sYPh+y4CDaK4m+iG7vatiVLoZZW+JTMMSLAkHXoRjNGwUjU9UVYnAgl0ZUg8Cu1YfN1asacuYTxbIKn5HlcFuZlfdCdlv3+/WfjDIhRbhCDSdoPC50puC/7OwIEU6nejFjk4OmvW+h8ikxl0bGyV14I7WXqy6DgfDdrm4mjHDHjo1rsLYzV4p/nlcE6/IuA4HOfOB1Kiq42JX5ZQ/fPLJAPTHxg3o4=
RSAUtils2.ts:235 前端:Decrypted text: k8SCMMALxjXUNeNwhn2IUW2sCK5FW6t6ZnUIj3o4WgMkgxHPdZZPcuGcyr2oTCnnVX327J+g8VT1AYNCJD3x+hJ46tGRzx9mWgdM3l0a8K8skXghFE/hqqWk+mvZk8QghZavqf3qim9sgWoqXDKZKS1G7QHYGsTD1zh3KPQ1Px0Gmue7pv5dZCK2MLeCMBj78PcoF9cdNsfSXPs9Ed7QrH6SXAQWyl/aLDm4iDVSzSXhr/LkQwxFFVD

注意事项:

这里面好多参数是为了和后端工具类保持一致,否则会导致前端加密的密文在后端工具类中无法解密的问题

  1. 后端使用的加密模型和填充方式是:RSA/ECB/OAEPWithSHA-256AndMGF1Padding

  2. 前端原生是没有这个填充方式的所以需要安装第三方依赖库:node-forge 具体体现为: > > ```typescript his.privateKey.decrypt( this.isValidBase64(data) ? this.decodeBase64(data) : data, “RSA-OAEP”, { md: forge.md.sha256.create(), mgf1: { md: forge.md.sha256.create() } } );

 >
 >    在这个代码中指定了填充方式为 OAEP sha-256 mgf1

解析:RSA/ECB/OAEPWithSHA-256AndMGF1Padding

  • RSA:它是加密算法的核心,RSA 是一种常用的非对称加密算法,可以提供非常强大的安全性。

  • ECB:它代表电子密码本模式(Electronic CodeBook),在这种模式下,每一个明文块都会独立地加密到一个密文块。

  • OAEP:是英文 “Optimal Asymmetric Encryption Padding”的简称,翻译为最优非对称加密填充。这是 RSA 加密中的一种填充方案,可以增加加密的安全性。

  • WithSHA-256AndMGF1Padding: 这部分描述了 OAEP 使用的 hash 函数和 mask 生成函数。SHA-256 是一种 hash 函数,可以将输入的数据转换为一个 256 bit 长度的 hash 值。MGF1 是一个基于 hash 函数的 mask 生成函数,它用于在 OAEP 中生成一个 mask。

RSA/ECB/PKCS1Padding

  • RSA/ECB/PKCS1Padding:这是一种早期的填充方案,它比较简单,但安全性较低。尤其是对于某些特定的攻击,如“填充攻击”,PKCS1Padding 存在一定的安全隐患。

  • RSA/ECB/OAEPWithSHA-256AndMGF1Padding: 这是一种更为先进的填充方案。OAEP(Optimal Asymmetric Encryption Padding)是在 PKCS1 基础上做了改进的一种方案,它增加了一个“masking”过程,可以有效地防止填充攻击,提高了加密的安全性。同时,采用SHA-256作为hash函数的OAEP,其安全性更高,因为SHA-256相比SHA-1更难以发生碰撞。

RSA除了ECB还支持啥,有什么区别吗?

  • ECB(Electronic Code Book,电子密码本模式):ECB模式是最早的块密码工作模式,其特点是将整个明文分成若干长度相等的块,相同的明文块经过相同的密钥加密后会得到相同的密文块。这种性质使得ECB模式容易受到重放攻击和字典攻击。

  • CBC(Cipher Block Chaining,密码分组链接模式):CBC模式在每次加密前,会将当前明文块和前一个密文块进行异或后再进行加密。这使得相同的明文块得到不同的密文,增加了安全性。

  • CFB(Cipher FeedBack,密文反馈模式)和OFB(Output FeedBack,输出反馈模式):在CFB和OFB模式中,前一块的密文会反馈到下一轮的运算中,增加了加密的安全性。

后端 RsaUtil.java

package cn.com.accumulate.common.encoded;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 官网地址:<a href="https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#cipher-algorithm-names">...</a>
 * @author wuhming
 * @description ""
 * @date 2024/4/25 17:45
 */
public class RSAUtil {
    /**
     * 加密算法RSA
     */
    private static final String KEY_ALGORITHM = "RSA";

    /**
     * 算法名称/加密模式/数据填充方式
     * 默认:RSA/ECB/PKCS1Padding
     */
    private static final String ALGORITHMS = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";

    /**
     * RSA最大加密明文大小
     */
    private static final int MAX_ENCRYPT_BLOCK = 190;

    /**
     * RSA最大解密密文大小
     */
    private static final int MAX_DECRYPT_BLOCK = 256;

    /**
     * RSA 位数 如果采用2048 上面最大加密和最大解密则须填写:  245 256
     */
    private static final int INITIALIZE_LENGTH = 2048;

    /**
     * 后端RSA的密钥对(公钥和私钥)Map,由静态代码块赋值
     */
    private static final Map<String, String> map = new LinkedHashMap<>(2);

    /**
     * 生成密钥对(公钥和私钥)
     */

    public static Map<String, String> genKeyPair() throws Exception {
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
        keyPairGen.initialize(INITIALIZE_LENGTH);
        KeyPair keyPair = keyPairGen.generateKeyPair();
        // 获取公钥
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        // 获取私钥
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        // 得到公钥字符串
        String publicKeyString = Base64.getEncoder().encodeToString(publicKey.getEncoded());
        // 得到私钥字符串
        String privateKeyString = Base64.getEncoder().encodeToString(privateKey.getEncoded());
        map.put("publicKey", publicKeyString);
        map.put("privateKey", privateKeyString);
        return map;
    }

    public static String getPrivateKey() {
        return map.get("privateKey");
    }

    public static String getPublicKey() {
        return map.get("publicKey");
    }

    /**
     * RSA私钥解密
     *
     * @param data       密文
     * @param privateKey 私钥(BASE64编码)
     * @return 明文
     */
    public static String decryptByPrivateKey(String data, String privateKey) throws Exception {
        //base64格式的key字符串转Key对象
        Key privateK = KeyFactory.getInstance(KEY_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
        Cipher cipher = Cipher.getInstance(ALGORITHMS);
        OAEPParameterSpec oaepParams = new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT);
        cipher.init(Cipher.DECRYPT_MODE, privateK, oaepParams);
//        cipher.init(Cipher.DECRYPT_MODE, privateK);
        // 先base64 解码成数组
        byte[] decodedData = Base64.getDecoder().decode(data);
        String encodeToString = new String(decodedData);
        //分段进行解密操作
        byte[] decryptData = encryptAndDecryptOfSubsection(decodedData, cipher, MAX_DECRYPT_BLOCK);
        return new String(decryptData);
    }

    /**
     * RSA公钥加密
     *
     * @param data      需要加密的原数据
     * @param publicKey 公钥(BASE64编码)
     * @return base64编码的字符串
     */
    public static String encryptByPublicKey(String data, String publicKey) throws Exception {
        //base64格式的key字符串转Key对象
        Key publicK = KeyFactory.getInstance(KEY_ALGORITHM).generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)));
        Cipher cipher = Cipher.getInstance(ALGORITHMS);
        // 初始化Cipher实例并配置OAEPParameterSpec
        OAEPParameterSpec oaepParams = new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT);
        // 使用OAEP参数
        cipher.init(Cipher.ENCRYPT_MODE, publicK, oaepParams);

        //分段进行加密操作
        byte[] encryptData = encryptAndDecryptOfSubsection(data.getBytes(StandardCharsets.UTF_8), cipher, MAX_ENCRYPT_BLOCK);
        return Base64.getEncoder().encodeToString(encryptData);
    }

    /**
     * RSA公钥解密
     *
     * @param data      BASE64编码过的密文
     * @param publicKey RSA公钥
     * @return utf-8编码的明文
     */
    public static byte[] pubKeyDec(byte[] data, String publicKey) throws Exception {
        //base64格式的key字符串转Key对象
        Key pubKey = KeyFactory.getInstance(KEY_ALGORITHM).generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)));
        Cipher cipher = Cipher.getInstance(ALGORITHMS);
        // 初始化Cipher实例并配置OAEPParameterSpec
        OAEPParameterSpec oaepParams = new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT);
        // 使用OAEP参数
        cipher.init(Cipher.ENCRYPT_MODE, pubKey, oaepParams);

        //分段进行解密操作
        return encryptAndDecryptOfSubsection(data, cipher, MAX_DECRYPT_BLOCK);
    }

    /**
     * RSA私钥加密
     *
     * @param data       待加密的明文
     * @param privateKey RSA私钥
     * @return 经BASE64编码后的密文
     */
    public static byte[] privateKeyEnc(byte[] data, String privateKey) throws Exception {

        //base64格式的key字符串转Key对象
        Key privateK = KeyFactory.getInstance(KEY_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
        Cipher cipher = Cipher.getInstance(ALGORITHMS);
        // 初始化Cipher实例并配置OAEPParameterSpec
        OAEPParameterSpec oaepParams = new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT);
        // 使用OAEP参数
        cipher.init(Cipher.ENCRYPT_MODE, privateK, oaepParams);

        //分段进行加密操作
        return encryptAndDecryptOfSubsection(data, cipher, MAX_ENCRYPT_BLOCK);
    }

    /**
     * 分段进行加密、解密操作
     */
    private static byte[] encryptAndDecryptOfSubsection(byte[] data, Cipher cipher, int encryptBlock) throws Exception {
        int inputLen = data.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // 对数据分段加密
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > encryptBlock) {
                cache = cipher.doFinal(data, offSet, encryptBlock);
            } else {
                cache = cipher.doFinal(data, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * encryptBlock;
        }
        out.close();
        return out.toByteArray();
    }


    public static void main(String[] args) throws Exception {
        Security.addProvider(new BouncyCastleProvider());
//        try {
//            genKeyPair();
//        } catch (Exception e) {
//            throw new RuntimeException(e);
//        }
//        String publicKey = getPublicKey();
//        String privateKey = getPrivateKey();
//        System.out.println("publicKey: " + publicKey);
//        System.out.println("privateKey: " + privateKey);

        String publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApUUT5pKm4IFrBT3s84pS9NET2pLkiiefgtFcKfm4s7qEUwfndeTqi32AL6KLFkvEo0afoJy2BTcMUESveeMhOf2xRun2fkYS9Pt4w8Qd3VHYRSyN/3ALn9uwvcPmqInLRROZv23POBWtbPib8ruRlUp986npuKv0kS6AdemW4wx30ImwOHKIMZsTkf5KPOLYtnpWRpiuQcTW4CdMTM+511e2zLaYmwZFpOXGANDZxCOJLVuM05ullZXRFejjVNXHkAos0bty8rznQY/moqdnHb6IyyYhv7qgU+epDIg3y6q/yCscv4kOB2xyMLzEmmV7dPAn4V29cMLKdKnfp1kIxQIDAQAB";
        String privateKey = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQClRRPmkqbggWsFPezzilL00RPakuSKJ5+C0Vwp+bizuoRTB+d15OqLfYAvoosWS8SjRp+gnLYFNwxQRK954yE5/bFG6fZ+RhL0+3jDxB3dUdhFLI3/cAuf27C9w+aoictFE5m/bc84Fa1s+Jvyu5GVSn3zqem4q/SRLoB16ZbjDHfQibA4cogxmxOR/ko84ti2elZGmK5BxNbgJ0xMz7nXV7bMtpibBkWk5cYA0NnEI4ktW4zTm6WVldEV6ONU1ceQCizRu3LyvOdBj+aip2cdvojLJiG/uqBT56kMiDfLqr/IKxy/iQ4HbHIwvMSaZXt08CfhXb1wwsp0qd+nWQjFAgMBAAECggEAIj6Xeom9mTxHvBzSOb5aJQ8jcYFJaJUAOL9/vNTZICiQHZJANA38SW32PyiLcFG/y2MpVULTdm4rF4C76WIWMultr7dv00dayzdGFS5QT9kTGnY6cAi75+JCE3gBlb+DhmsiyucKbmETf8ZdreSOrGP0iCYXXyQGN4Tuqj/9+0p3T9LoDzTkbTNxdzI1yHr87EsZLB07Xk/z1ehaQFiXa40JYuj5nJRh321zi0F5MMCE7UWA1HsKLJ2rVhkDPcWY0kPbR3CXZ6o+47V8c8CLgGVd21Ic2QjQZfqfbUZZRWyT1AziBzrwJKC6EocN014Qcw3jWH7vt/2yIRfzx4ji4QKBgQDJI61qLaOGf0OqXQoiZeWhagsvQghhIxtfmsdIhDLDzR8l7ooa8K7Jb2gMlT1iFgTpo6D0chNhXkABVMiEbAo8s6aSupdRRfXvt3sriNjAPLsOvWB0hZy5TgOYctTJ31E+ltnmKFu9+ZSpO0Dnae7Vs62Ce/VOPH1UVdXeqWvsUQKBgQDSWNogbjJt5UgoFvAozp0euquoi6iVN9xpFmlL3qdtpyBjsxmGgYSZSqpICmwWOIRpMRkIOtm93WVAjzWXdbdoI2HUOWXG5eB90C8tVs/rapQmuYOUHQtHKZPESiZg1biBGJHwnpFOSkjiKymRkPMmMOUrq9ozq82/0Q/0mNRcNQKBgB+FBcHj0QJcox/pYJMvYKLA2WvYbzO5qqp6uMv2W3YviLgK/31lRh6u5KFEKdBbHv319F0aG0tg16rdvGZz/86vYGUR9HB6BCZCnHyf/0dwvOd8accYwQyQ2yNiq1HPOw4R2aeln2rP16oFbRBWm2TFuWT/Q9u0wvcaVF+GMTFhAoGAIATWb0lW89OLAE89j3ikAiF4L9fSeaRfML/wl1P1OIPQMlz47biZbpwm/4BWo6tGvJx0qwQKURR6bTFffwa0PG3mXl++VmB3o68k/0nPrI40rxBeLxWcueBiVebToLnns/8BeBcrFzfHkGXxle7ylEWtHNC0DlmAoEo6m4kICmECgYAkQZaDIOsmobDywgTTg59rXix9Rif9NBKHZGWjTWz1QkdmuWFG4vpAsK5GyJ32jF0CTtvRkTcVRkSIpm9OxI2pkWePvDspA83PRLsWuycAXUZqJy/3tLgkeUjt19q3v1FXOqbXWzISUJ/gve6xSaPMBJfAgDNFS4stioq31+/QOQ==";

        String aesKey = "z8dP0WUUXCHzL8nGCdK1WwUU3iEPYgBvhLt15j1Rs9GYr6bGjbFeg97zwobs88nG8IvXCGcrtuRvxT9Haf6KqDgamMo5DIpUpMNWoXT1Y6jGvjgWZvagcsN4btajU93IvQgborMeXFAQoggsqL04xCfxSnvJjDawPsSJ3qtoWa2bV6N6c8WXuwWZAWYwWSYihrP4jSz0Tg7EDftMkDPzlUYuKoqQ8Rs0lnFFlsD5n0nGvNYKWP4IdoKXcafgp8wP";

        String encodeToString = encryptByPublicKey(aesKey, publicKey);
        System.out.println(encodeToString);
        String decrypt = decryptByPrivateKey(encodeToString, privateKey);
        System.out.println(decrypt);

        String aa = "lA1+gP6zmKskiS3pRejswG9PPgmy+96Z0656pTEaqd/qeQEthyKcRvTJe9McHuvf9mN9kTBoBp5WgUhI8QN7gAYWlNip3HNq97X+ZcUE28S1kzl+VSplFH+xCAlQFQ1egZLX0U6tOTpj8VPHm67nhFed86O91lB7r4zoe/EraNTLRpbvvREDQiGUAOrRGjX+d/jJtpApjKqYyaiv6zAYbza1kKySvRveEbNms/obiAABHAkKcrSgP/bbl0ebuhQ/XVGZhONU4JmxqQz1hr4Vt8jIWYu8fecIEj1pqstsx6nHeHd9xv6XWi1vbq61E8/MsIaTxc3BTqc4WRpSNHDDECa1qApicGk7nzMVCu8meB7HkrsJBKUiFFavO7Cpz8rHpsOoU5cQ9/Qwaw8MXSLVYqldNirKFkVXjAXGkNf1vXvTz1/I05Bzit+2XQgiJ8A/uFx0h/KQK4LfUOVwCcDB41c177Vs+bxDtJugefUV65iug7Gc1HMcu63VfKT374Pas0AnImfrRBVm8RnUxrsUVLBbkvrhd9zo/Z3ChfgbzQL5LoHGlBQd1WQt8AfR9E4UU5SHrIxeDRJDBsTr5vUhuBwYVlckUMldXCjfVGoSFAustnj4JF87NkyElwLR+vs5drUn+VSttHBZ+J5fXu33VeFHi5JSGNMJ+4WoWIUs2EU=";
//        String encodeToString1 = Base64.getEncoder().encodeToString(aa.getBytes(StandardCharsets.UTF_8));
        String bb = decryptByPrivateKey(aa, privateKey);
        System.out.println(bb);
    }
}

将前端密文复制到后端:

bjam2nCRsvAOWdhXdg7h5SGcnOHggroTYM3MQdlQlQ/5idricU1s3CXUblDP88KvFNwwdUkNUx70MOYFt4r4xg8ygvcx/KTJBIgMcTSn/6xEbq65vs496gCW7N2lo+R58fb46iZPnr9vZhLNoRkG5XWWV1y5MI1zTJA+/WbdNx/YWOAChPyS5dQ1YiLGCq41CV+AAmfYNnlXhZ/7wNOVkbuVAmEjfRCC7A8bRUfKh0WWsJNxgFlIkmRaiv+2dW2ti/ZbBN+M8rAetbay1o6ko2z9A8hhCI+7LAfy2WKhRHaq5pvL67pUkgSknfGQc9V7tuFUNbqrgirxYgQhzmVXi2szxWaukXz2Jk6CgWurctZ85f6RLQb6RQX0JyQsgPiE21DdX5J4/SUNC3/oWZ6DlfZnSqDDCvF8/gPn9TZgdTg2iKO2Ef5cgCkWE6pmxT6jQLOcspFWNJ3hewigYnohL1ztp8JVqdMuSQYMq4+V4JKJ98CU5inDiC0ZXOilvg51RayJ6iu8dMa8FzmaKnL1wfZHTvG4UF3jxVr1SAdotpph7zYso2s0f4q7V/ImFAM2P80EjZS1RV5wMFTrBp21QPoJYxFvnCSRXDtDPnZL3i30n8H0dD/aiINpcy+pmQsb8HVusnVYs531kvyxt2nL6XJePnHjv3zpSdTgE8+D6/A=

z8dP0WUUXCHzL8nGCdK1WwUU3iEPYgBvhLt15j1Rs9GYr6bGjbFeg97zwobs88nG8IvXCGcrtuRvxT9Haf6KqDgamMo5DIpUpMNWoXT1Y6jGvjgWZvagcsN4btajU93IvQgborMeXFAQoggsqL04xCfxSnvJjDawPsSJ3qtoWa2bV6N6c8WXuwWZAWYwWSYihrP4jSz0Tg7EDftMkDPzlUYuKoqQ8Rs0lnFFlsD5n0nGvNYKWP4IdoKXcafgp8wP

k8SCMMALxjXUNeNwhn2IUW2sCK5FW6t6ZnUIj3o4WgMkgxHPdZZPcuGcyr2oTCnnVX327J+g8VT1AYNCJD3x+hJ46tGRzx9mWgdM3l0a8K8skXghFE/hqqWk+mvZk8QghZavqf3qim9sgWoqXDKZKS1G7QHYGsTD1zh3KPQ1Px0Gmue7pv5dZCK2MLeCMBj78PcoF9cdNsfSXPs9Ed7QrH6SXAQWyl/aLDm4iDVSzSXhr/LkQwxFFVD

可以实现前端加密,后端解密:

前端AESUtil.ts

import * as forge from "node-forge";

export class AESUtils {

  public generateKey(bitLength = 16): string {
    if (![16, 24, 32].includes(bitLength)) {
      throw new Error(
        "Invalid key size. Key size must be either 128, 192, or 256 bits."
      );
    }
    return this.randomBase64(bitLength).substring(0, bitLength);
  }

  // 生成一个安全的随机IV(16个字符)
  public generateIv(): string {
    return this.randomBase64(16).substring(0, 16);
  }

  // 将十六进制字符串密钥转换为二进制Buffer
  private getKeyBytes(keyHex: string): forge.util.ByteBuffer {
    return forge.util.createBuffer(keyHex, "raw");
  }

  /**
   * @description: 加密方法
   * @param {string} text
   * @param {string} aesKey
   * @param {string} ivString
   * @return {*}
   */
  public encrypt(text: string, aesKey: string, ivString: string): string {
    const key = this.getKeyBytes(aesKey);
    const iv = this.getKeyBytes(ivString);

    // 创建cipher
    const cipher = forge.cipher.createCipher("AES-CBC", key);
    cipher.start({ iv: iv });
    cipher.update(forge.util.createBuffer(text, "utf8"));
    // 采用PKCS#7填充模式,在node-forge中,当你使用cipher对象的finish方法时,默认情况下它会采用PKCS#7填充模式来完成加密过程
    cipher.finish();

    // 加密文本
    const encrypted = cipher.output;
    return forge.util.encode64(encrypted.getBytes());
  }

  // 解密方法
  public decrypt(
    encryptedBase64: string,
    aesKey: string,
    ivString: string
  ): string {
    const key = this.getKeyBytes(aesKey);
    const iv = this.getKeyBytes(ivString);
    const encryptedBytes = forge.util.createBuffer(
      forge.util.decode64(encryptedBase64),
      "raw"
    );

    // 创建decipher
    const decipher = forge.cipher.createDecipher("AES-CBC", key);
    decipher.start({ iv: iv });
    decipher.update(encryptedBytes);
    // 采用PKCS#7填充模式,在node-forge中,当你使用cipher对象的finish方法时,默认情况下它会采用PKCS#7填充模式来完成加密过程
    const result = decipher.finish();

    if (!result) {
      throw new Error("Failed to decrypt message");
    }
    // 解密文本
    return decipher.output.toString();
  }

  // 根据byteLength生成指定长度的base64编码字符串
  private randomBase64(byteLength: number): string {
    return forge.util.bytesToHex(forge.random.getBytesSync(byteLength));
  }
}

// 使用示例:
try {
  const aesutil =  new AESUtils();
  const textToEncrypt = "Hello, world!"; // 明文
  const key = aesutil.generateKey(); // 生成密钥
  const iv = aesutil.generateIv(); // 生成IV
  console.log("aesKey: ", key);
  console.log("iv: ", iv);

  const encryptedText = aesutil.encrypt(textToEncrypt, key, iv); // 加密
  console.log("Encrypted text:", encryptedText);

  const decryptedText = aesutil.decrypt(encryptedText, key, iv); // 解密
  console.log("Decrypted text:", decryptedText);

  if (textToEncrypt === decryptedText) {
    console.log("Encryption and decryption are successful.");
  } else {
    console.log("Encrypted and decrypted text do not match!");
  }
} catch (error) {
  console.error("An error occurred:", error);
}

前端日志:

aesKey:  005606ce27f0e425
iv:  75b39204ef7c1ee3
Encrypted text: MFi7J97BUQ0g+LpoV+M35A==
Decrypted text: Hello, world!
Encryption and decryption are successful.

后端AESUtil.java

需要引入依赖:

<!-- Bouncycstle密码包-->
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
</dependency>
@Slf4j
public class AESUtil {

    private final String ALGORITHM = "AES";

    /**
     * 是否初始化
     */
    private boolean initialized;

    /**
     * 加密/解密模式:CBC,FBC,ECB
     */
    private final Model mode;

    public AESUtil() {
        this.mode = Model.ECB;
        this.initialized = false;
    }

    public AESUtil(Model mode) {
        this.mode = mode;
        this.initialized = false;
    }


    /**
     * 也可以通过这种方式获取密钥
     *
     * @param key
     * @return
     */
    private SecretKey getSecretKey(String key) {
        try {
            //获取指定的密钥生成器
            KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
            //加密强随机数
            SecureRandom secureRandom = new SecureRandom();
            secureRandom.setSeed(key.getBytes(StandardCharsets.UTF_8));
            //这里可以是128、192、256、越大越安全
            keyGen.init(256, secureRandom);
            return keyGen.generateKey();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("AES获取密钥出现错误,算法异常");
        }
    }

    /**
     * @param key   密钥,key长度必须大于等于 3*8 = 24,并且是8的倍数
     * @param keyIv 初始化向量,keyIv长度必须等于16
     * @param data  明文
     * @return 密文
     */
    public String encoded(String key, String keyIv, String data) {
        initialize();
        try {
            //获取SecretKey对象,也可以使用getSecretKey()方法
            Key secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
            //获取指定转换的密码对象Cipher(参数:算法/工作模式/填充模式)
            Cipher cipher = Cipher.getInstance(this.mode.getCode());
            if (StringUtils.isNotEmpty(keyIv)) {
                //创建向量参数规范也就是初始化向量
                IvParameterSpec ips = new IvParameterSpec(keyIv.getBytes(StandardCharsets.UTF_8));
                //用密钥和一组算法参数规范初始化此Cipher对象(加密模式)
                cipher.init(Cipher.ENCRYPT_MODE, secretKey, ips);
            } else {
                //用密钥和一组算法参数规范初始化此Cipher对象(加密模式)
                cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            }
            //执行加密操作
            byte[] bytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
            // 将二进制转换转为base64编码返回
            Base64.Encoder encoder = Base64.getEncoder();
            return encoder.encodeToString(bytes);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * @param key   密钥,key长度必须大于等于 3*8 = 24,并且是8的倍数
     * @param keyIv 初始化向量,keyIv长度必须等于16
     * @param data  密文
     * @return 明文
     */
    public String decoded(String key, String keyIv, String data) {
        this.initialize();
        try {
            //获取SecretKey对象,也可以使用getSecretKey()方法
            Key secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
            //获取指定转换的密码对象Cipher(参数:算法/工作模式/填充模式)
            Cipher cipher = Cipher.getInstance(this.mode.getCode());
            if (StringUtils.isNotEmpty(keyIv)) {
                //创建向量参数规范也就是初始化向量
                IvParameterSpec ips = new IvParameterSpec(keyIv.getBytes(StandardCharsets.UTF_8));
                //用密钥和一组算法参数规范初始化此Cipher对象(加密模式)
                cipher.init(Cipher.DECRYPT_MODE, secretKey, ips);
            } else {
                cipher.init(Cipher.DECRYPT_MODE, secretKey);
            }
            // 将base64编码转为二进制数组
            Base64.Decoder decoder = Base64.getDecoder();
            byte[] bytes = decoder.decode(data);
            //执行解密操作
            return new String(cipher.doFinal(bytes));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * 初始化
     */
    private void initialize() {
        if (initialized) {
            return;
        }
        Security.addProvider(new BouncyCastleProvider());
        initialized = true;
        log.debug("org.bouncycastle.jce.provider.BouncyCastleProvider:初始化成功");
    }


    @Getter
    public enum Model {

        /**
         * ECB 模式:不需要偏移量iv
         */
        ECB("AES/ECB/PKCS7Padding"),

        /**
         * CBC: 需要偏移量
         */
        CBC("AES/CBC/PKCS7Padding"),

        /**
         * CFB:需要偏移量
         */
        CFB("AES/CFB/PKCS7Padding"),

        ;

        private final String code;

        Model(String code) {
            this.code = code;
        }
    }

    //
    public static void main(String[] args) {
//        String aesKey = GeneratorCodeUtil.getRandomStr(24);
//        String iv = GeneratorCodeUtil.getRandomStr(16);
        // Key length not 128/192/256 bits  16/24/32 位
        String aesKey = "005606ce27f0e425";
        String iv = "75b39204ef7c1ee3";
        String password = "123qazwsx";
        AESUtil aesUtil = new AESUtil(Model.CBC);
        String encoded = aesUtil.encoded(aesKey, iv, password);
        System.out.println(encoded);
        System.out.println(aesUtil.decoded(aesKey, iv, encoded));

        System.out.println(aesUtil.decoded(aesKey, iv, "MFi7J97BUQ0g+LpoV+M35A=="));
    }

}

后端日志:

+EkApLxdlBKtOzmoHlSkZg==
123qazwsx
Hello, world!

生产运用案例:登录用户密码加密(一)

后端获取公钥接口

@Operation(summary = "获取公钥")
@GetMapping("/getPublicKey")
public RestResponse<String> getPublicKey(){
    // 需要考虑并发的问题
    String publicKey = RSAUtil.getPublicKey();
    if(StringUtils.isBlank(publicKey)){
        LOCK.lock();
        try {
            publicKey = RSAUtil.getPublicKey();
            if(StringUtils.isBlank(publicKey)){
                RSAUtil.genKeyPair();
                publicKey = RSAUtil.getPublicKey();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }finally {
            LOCK.unlock();
        }
    }
    return RestResponse.ok(publicKey);
}

后端登录接口

@Data
public class AuthLoginRestInput {

    private String username;

    private String passwordEncrypt;

    private String ivEncrypt;

    private String aesKeyEncrypt;

}


@Operation(summary = "登录")
@PostMapping("login")
@SysLog(title = "登录", businessType = BusinessType.LOGIN, isSaveRequestData = false)
public RestResponse<AuthLoginRestOutput> login(@RequestBody AuthLoginRestInput restInput) {
    AuthLoginRestOutput output = userService.login(restInput);
    return RestResponse.ok(output);
}

service

@Override
public AuthLoginRestOutput login(AuthLoginRestInput restInput) {


    User user = this.checkLogin(restInput);
    // todo 登录信息
    StpUtil.login(user.getId(), SaLoginConfig);
    // todo 生成token
    

    // todo 组装用户信息给前端
 
    return restOutput;
}

StpUtil工具来自于框架 sa-token这里不做过多介绍。

this.checkLogin(restInput) 解密的部分:

private User checkLogin(AuthLoginRestInput restInput){
  if(StringUtils.isBlank(restInput.getUsername())){
      throw new CEIECException(BizExceptionEnum.USER_NAME_IS_NULL);
  }
  User user = this.getUserByUserName(restInput.getUsername());
  if(user == null){
      throw new CEIECException(BizExceptionEnum.USER_NOT_EXIST);
  }
  // 解密
  String privateKey = RSAUtil.getPrivateKey();
  String password = "";
  try {
      String aesKey = RSAUtil.decryptByPrivateKey(restInput.getAesKeyEncrypt(), privateKey);
      String iv = RSAUtil.decryptByPrivateKey(restInput.getIvEncrypt(), privateKey);
      log.info("[iv={}], [aesKey={}], [passwordEncrypt={}]", iv, aesKey, restInput.getPasswordEncrypt());
      AESUtil aesUtil = new AESUtil(AESUtil.Model.CBC);
      password = aesUtil.decoded(aesKey, iv, restInput.getPasswordEncrypt());
      log.info("[password={}]", password);
  }catch (Exception e){
      log.error("error: ", e);
  }

  if(!BCrypt.checkpw(password, user.getUserPwd())){
      log.debug("密码错误");
      throw new CEIECException(BizExceptionEnum.PASSWORD_ERROR);
  }

  if(Constants.NO.equals(user.getUserStatus())){
      log.debug("用户已被冻结");
      throw new CEIECException(BizExceptionEnum.USER_ACCOUNT_LOCKED);
  }
  return user;
}

前端代码(部分核心代码)

在前端的两个工具类中导出一个类对象

// RSAUtilsForge.ts

export const rsaUtil = new RSAUtilsForge();

// AESUtil.ts

export const aesUtil = new AESUtils();

在登录页面的登录方法中将密码加密后传输:

const onLogin = async (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  await formEl.validate(async (valid, fields) => {
    if (valid) {
      loading.value = true;
      // 获取aseKey
      const aesKey = aesUtil.generateKey();
			// 获取偏移量
      const aesIv = aesUtil.generateIv();
			// 使用AES算法解密用户密码
      const passwordEncrypt = aesUtil.encrypt(ruleForm.password, aesKey, aesIv);
			// 从后端获取公钥  这里采用的是pinia,存储到内存中,如果内存中不存在再请求后台接口获取后放入内存中
      const publicKey = await useUserStoreHook().GET_PUBLICKEY();
     	// 设置公钥
      rsaUtil.setPublicKey(publicKey);
			// 使用RSA算法将aesKey和Iv加密后一起传输给后端
      const ivEncrypt = rsaUtil.encrypt(aesIv, true);
      const aesKeyEncrypt = rsaUtil.encrypt(aesKey, true);

      useUserStoreHook()
        .loginByUsername({
          username: ruleForm.username,
          passwordEncrypt: passwordEncrypt,
          ivEncrypt: ivEncrypt,
          aesKeyEncrypt: aesKeyEncrypt
        })
        .then(res => {
          if (res.success) {
            // 获取后端路由
            return initRouter().then(() => {
              router.push(getTopMenu(true).path).then(() => {
                message(t("login.pureLoginSuccess"), { type: "success" });
              });
            });
          } else {
            message(t(res.message), { type: "error" });
          }
        })
        .finally(() => (loading.value = false));
    } else {
      return fields;
    }
  });
};

前端请求:

后端日志

c.c.a.b.t.p.s.impl.UserServiceImpl       : [iv=1bb1eb499c2650a9], [aesKey=d9470fbb8d4c0aa3], [passwordEncrypt=0raIFuo+HjgD0pSef6Zubw==]
c.c.a.b.t.p.s.impl.UserServiceImpl       : [password=123456]

生产运用案例:用户敏感数据加密传输(二)

场景:在一些网络请求中可能会包含用户的银行卡或手机号,住址等个人隐私数据,有些时候是可以通过脱敏来解决,有些场景会将整个json对象格式化成字符串然后整个串加密

思路:在请求头中添加一个标识符isEncrypt,默认是false,在axios发送请求前拦截器中根据这个标志位来判断是否需要对数据加密;后端可以在拦截器中对请求拦截解密之后再交给controller

0%