采用AES和RSA混合模式传输敏感数据
采用AES和RSA混合模式传输敏感数据
简介
什么是AES算法
AES是对称加密算法,其安全性也经得住考验,主要优点是加解密效率高,适合大规模数据的加密。
什么是RSA算法
RSA是非对称加密算法,主要优点是安全性较高。但是,非对称加密算法的加解密效率较低,而且加密数据长度受到限制。
两者的区别
- AES加密算法:
- 种类:AES是对称加密算法,数据发送者和接收者使用同一个密钥进行加密和解密。
- 优点:AES加密和解密效率非常高,能够快速地处理大量数据,这使得AES非常适合在需要对大量数据进行加密的场合使用。
- 缺点:AES的主要安全问题是密钥的管理和分发。如果密钥在被发送给接收者的过程中被截获,那么数据的安全性将无法得到保证。
- 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
注意事项:
这里面好多参数是为了和后端工具类保持一致,否则会导致前端加密的密文在后端工具类中无法解密的问题
后端使用的加密模型和填充方式是:RSA/ECB/OAEPWithSHA-256AndMGF1Padding
前端原生是没有这个填充方式的所以需要安装第三方依赖库: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