AES + RSA 实现前后端数据加密传输
参考:https://www.cnblogs.com/privateLogs/p/17888711.html
环境:
- 后端 spring boot
- 前端 vue3 + axios
对称加密:加密和解密用同一组密钥。AES(一组 key,进行加密和解密)
非对称加密:加密和解密不用同一组密钥。RSA(公钥加密,私钥解密)
核心思想
后端:
- 后端RSA:公钥public1,私钥private1
- 后端AES:aes1
- 后端加密数据:data1
前端:
- 前端RSA:公钥public2,私钥private2
- 前端AES:aes2
- 前端加密数据:data2
步骤:
- 后端生成public1,private1
- 前端请求后端,得到public1
- 前端生成public2,private2,aes2
- 前端用 aes2 加密 data2,用 public1 加密 aes2
- 前端请求数据:加密data2,加密aes2,public2
- 后端使用 private1 解密 aes2
- 后端使用 aes2 解密 data2
- 后端生成 aes1
- 后端用 aes1 加密响应 data1,使用 public2 对 aes1 加密
- 后端响应数据:加密 data1,加密 aes1
- 前端使用 private2 解密 aes1,用 aes1 解密 data1
AES
高级加密标准(英语:Advanced Encryption Standard,缩写:AES)
- 对称加密
- 密钥的长度可以使用128位、192位或256位
- AES为分组密码,分组密码也就是把明文分成一组一组的,每组长度相等,每次加密一组数据,直到加密完整个明文
- 在AES标准规范中,分组长度只能是128位
- 加密和解密的运算速度较快,消耗资源较少
- 适用于加密大量数据,如文件加密、网络通信加密
AES的加密方式会将明文拆分成不同的块进行加密,例如一个256 位的数据用128的密钥加密,则分成
明文1(128位) 明文2(128位)
加密
密文1(128位) 密文2(128位)
填充
如果明文不是128位(16字节)的则需要填充,即在明文某个地方补充到16个字节整数倍的长度,加解密时需要采用同样的填充方式,否则无法解密成功,以下是几种填充方式
NoPadding
不进行填充,但是这里要求明文必须要是16个字节的整数倍,这个可以使用者本身自己去实现填充,除了该种模式以外的其他填充模式,如果已经是16个字节的数据的话,会再填充一个16字节的数据
PKCS5Padding(默认)
在明文的末尾进行填充,填充的数据是当前和16个字节相差的数量,例如: 未填充明文 1,2,3,4,5,6,7,8,9,10,11
填充明文(缺少五个满足16个字节) 1,2,3,4,5,6,7,8,9,10,11,5,5,5,5,5
由于使用PKCS7Padding/PKCS5Padding填充时,最后一个字节肯定为填充数据的长度,所以在解密后可以准确删除填充的数据
ISO10126Padding
在明文的末尾进行填充,当前和16个字节相差的数量填写在最后,其余字节填充随机数,例如: 未填充明文 1,2,3,4,5,6,7,8,9,10,11
填充明文(缺少五个满足16个字节) 1,2,3,4,5,6,7,8,9,10,11,c,b,4,1,5
模式
模式是需要制定AES对明文进行加密时使用的模式(这里并不涉及具体的加密方法,只是加密步骤上的不同模式,在加解密时同样需要相同的模式,否则无法成功),一共提供了五种模式,模式的基本原理是近似的,但是细节上会有一些变化,
- ECB模式(默认)
- CBC模式
- CFB模式
- OFB模式
- CTR模式
具体情况自行查阅资料
java 实现 AES 加解密
- 加密算法:
String KEY_ALGORITHM = "AES";
- 分组长度:
int KEY_LENGTH = 16 * 8;
- 算法名称/加密模式/数据填充方式:
String ALGORITHMS = "AES/ECB/PKCS5Padding";
依赖库:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
AES Key 生成
/**
* 获取key
*/
public static String getKey() {
int length = KEY_LENGTH / 8;
StringBuilder uid = new StringBuilder(length);
//产生16位的强随机数
Random rd = new SecureRandom();
for (int i = 0; i < length; i++) {
//产生0-2的3位随机数
switch (rd.nextInt(3)) {
case 0:
//0-9的随机数
uid.append(rd.nextInt(10));
break;
case 1:
//ASCII在65-90之间为大写,获取大写随机
uid.append((char) (rd.nextInt(26) + 65));
break;
case 2:
//ASCII在97-122之间为小写,获取小写随机
uid.append((char) (rd.nextInt(26) + 97));
break;
default:
break;
}
}
return uid.toString();
}
AES 加密
/**
* 加密
*
* @param content 加密的字符串
* @param encryptKey key值
*/
public static String encrypt(String content, String encryptKey) throws Exception {
//设置Cipher对象
Cipher cipher = Cipher.getInstance(ALGORITHMS, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), KEY_ALGORITHM));
//调用doFinal
// 转base64
return Base64.encodeBase64String(cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)));
}
AES 解密
/**
* 解密
*
* @param encryptStr 解密的字符串
* @param decryptKey 解密的key值
*/
public static String decrypt(String encryptStr, String decryptKey) throws Exception {
//base64格式的key字符串转byte
byte[] decodeBase64 = Base64.decodeBase64(encryptStr);
//设置Cipher对象
Cipher cipher = Cipher.getInstance(ALGORITHMS,PROVIDER);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), KEY_ALGORITHM));
//调用doFinal解密
return new String(cipher.doFinal(decodeBase64));
}
整体代码
import org.apache.tomcat.util.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Random;
/**
* AES加、解密算法工具类
* 对称加密
*/
public class AesUtil {
/**
* 加密算法AES
*/
private static final String KEY_ALGORITHM = "AES";
/**
* key的长度,Wrong key size: must be equal to 128, 192 or 256
* 传入时需要16、24、36
*/
private static final int KEY_LENGTH = 16 * 8;
/**
* 算法名称/加密模式/数据填充方式
* 默认:AES/ECB/PKCS5Padding
*/
private static final String ALGORITHMS = "AES/ECB/PKCS5Padding";
/**
* 后端AES的key,由静态代码块赋值
*/
public static String key;
/**
* 不能在代码中创建
* JceSecurity.getVerificationResult 会将其put进 private static final Map<Provider,Object>中,导致内存缓便被耗尽
*/
private static final BouncyCastleProvider PROVIDER = new BouncyCastleProvider();
static {
key = getKey();
}
/**
* 获取key
*/
public static String getKey() {
int length = KEY_LENGTH / 8;
StringBuilder uid = new StringBuilder(length);
//产生16位的强随机数
Random rd = new SecureRandom();
for (int i = 0; i < length; i++) {
//产生0-2的3位随机数
switch (rd.nextInt(3)) {
case 0:
//0-9的随机数
uid.append(rd.nextInt(10));
break;
case 1:
//ASCII在65-90之间为大写,获取大写随机
uid.append((char) (rd.nextInt(26) + 65));
break;
case 2:
//ASCII在97-122之间为小写,获取小写随机
uid.append((char) (rd.nextInt(26) + 97));
break;
default:
break;
}
}
return uid.toString();
}
/**
* 加密
*
* @param content 加密的字符串
* @param encryptKey key值
*/
public static String encrypt(String content, String encryptKey) throws Exception {
//设置Cipher对象
Cipher cipher = Cipher.getInstance(ALGORITHMS, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), KEY_ALGORITHM));
//调用doFinal
// 转base64
return Base64.encodeBase64String(cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)));
}
/**
* 解密
*
* @param encryptStr 解密的字符串
* @param decryptKey 解密的key值
*/
public static String decrypt(String encryptStr, String decryptKey) throws Exception {
//base64格式的key字符串转byte
byte[] decodeBase64 = Base64.decodeBase64(encryptStr);
//设置Cipher对象
Cipher cipher = Cipher.getInstance(ALGORITHMS,PROVIDER);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), KEY_ALGORITHM));
//调用doFinal解密
return new String(cipher.doFinal(decodeBase64));
}
}
vue3 实现 AES 加解密
依赖库:crypto-js
AES Key 生成
function genKey(length = 16) {
const random = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let str = "";
for (let i = 0; i < length; i++) {
str = str + random.charAt(Math.random() * random.length)
}
return str;
}
AES 加密
//加密
function encrypt(plaintext: any, key: string) {
if (plaintext instanceof Object) {
//JSON.stringify
plaintext = JSON.stringify(plaintext)
}
const encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(plaintext), CryptoJS.enc.Utf8.parse(key), {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
}
AES 解密
//解密
function decrypt(ciphertext: any, key: any) {
const decrypt = CryptoJS.AES.decrypt(ciphertext, CryptoJS.enc.Utf8.parse(key), {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
let decString = CryptoJS.enc.Utf8.stringify(decrypt).toString();
if (decString.charAt(0) === "{" || decString.charAt(0) === "[") {
//JSON.parse
decString = JSON.parse(decString);
}
return decString;
}
整体代码
import CryptoJS from 'crypto-js';
function genKey(length = 16) {
const random = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let str = "";
for (let i = 0; i < length; i++) {
str = str + random.charAt(Math.random() * random.length)
}
return str;
}
//加密
function encrypt(plaintext: any, key: string) {
if (plaintext instanceof Object) {
//JSON.stringify
plaintext = JSON.stringify(plaintext)
}
const encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(plaintext), CryptoJS.enc.Utf8.parse(key), {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
}
//解密
function decrypt(ciphertext: any, key: any) {
const decrypt = CryptoJS.AES.decrypt(ciphertext, CryptoJS.enc.Utf8.parse(key), {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
let decString = CryptoJS.enc.Utf8.stringify(decrypt).toString();
if (decString.charAt(0) === "{" || decString.charAt(0) === "[") {
//JSON.parse
decString = JSON.parse(decString);
}
return decString;
}
export default {
genKey,
encrypt,
decrypt
}
RSA
RSA 加密
- 非对称加密(公钥加密,私钥解密)
- 运算速度慢
- 常用于加密较小的数据
java 实现 RSA 加解密
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.tomcat.util.codec.binary.Base64;
/**
* RSA加、解密算法工具类
* 非对称加密
*/
public class RsaUtil {
/**
* 加密算法
*/
private static final String KEY_ALGORITHM = "RSA";
/**
* 算法名称/加密模式/数据填充方式
* 默认:RSA/ECB/PKCS1Padding
*/
private static final String ALGORITHMS = "RSA/ECB/PKCS1Padding";
/**
* Map获取公钥的key
*/
private static final String PUBLIC_KEY = "publicKey";
/**
* Map获取私钥的key
*/
private static final String PRIVATE_KEY = "privateKey";
/**
* RSA最大加密明文大小
*/
private static final int MAX_ENCRYPT_BLOCK = 117;
/**
* RSA最大解密密文大小
*/
private static final int MAX_DECRYPT_BLOCK = 128;
/**
* RSA 位数 如果采用2048 上面最大加密和最大解密则须填写: 245 256
*/
private static final int INITIALIZE_LENGTH = 1024;
/**
* 后端RSA的密钥对(公钥和私钥)Map,由静态代码块赋值
*/
private static final Map<String, Object> genKeyPair = new LinkedHashMap<>(2);
static {
try {
genKeyPair.putAll(genKeyPair());
} catch (Exception e) {
//输出到日志文件中
//log.error(ErrorUtil.errorInfoToString(e));
}
}
/**
* 生成密钥对(公钥和私钥)
*/
private static Map<String, Object> 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();
Map<String, Object> keyMap = new HashMap<String, Object>(2);
//公钥
keyMap.put(PUBLIC_KEY, publicKey);
//私钥
keyMap.put(PRIVATE_KEY, privateKey);
return keyMap;
}
/**
* 私钥解密
*
* @param encryptedData 已加密数据
* @param privateKey 私钥(BASE64编码)
*/
public static byte[] decryptByPrivateKey(byte[] encryptedData, String privateKey) throws Exception {
//base64格式的key字符串转Key对象
Key privateK = KeyFactory.getInstance(KEY_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));
//设置加密、填充方式
Cipher cipher = Cipher.getInstance(ALGORITHMS);
cipher.init(Cipher.DECRYPT_MODE, privateK);
//分段进行解密操作
return encryptAndDecryptOfSubsection(encryptedData, cipher, MAX_DECRYPT_BLOCK);
}
/**
* 公钥加密
*
* @param data 源数据
* @param publicKey 公钥(BASE64编码)
*/
public static byte[] encryptByPublicKey(byte[] data, String publicKey) throws Exception {
//base64格式的key字符串转Key对象
Key publicK = KeyFactory.getInstance(KEY_ALGORITHM).generatePublic(new X509EncodedKeySpec(Base64.decodeBase64(publicKey)));
//设置加密、填充方式
Cipher cipher = Cipher.getInstance(ALGORITHMS);
cipher.init(Cipher.ENCRYPT_MODE, publicK);
//分段进行加密操作
return encryptAndDecryptOfSubsection(data, cipher, MAX_ENCRYPT_BLOCK);
}
/**
* 获取私钥
*/
public static String getPrivateKey() {
Key key = (Key) genKeyPair.get(PRIVATE_KEY);
return Base64.encodeBase64String(key.getEncoded());
}
/**
* 获取公钥
*/
public static String getPublicKey() {
Key key = (Key) genKeyPair.get(PUBLIC_KEY);
return Base64.encodeBase64String(key.getEncoded());
}
/**
* 分段进行加密、解密操作
*/
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;
// 对数据分段解密
while (inputLen - offSet > 0) {
int chunkSize = Math.min(encryptBlock, inputLen - offSet);
cache = cipher.update(data, offSet, chunkSize); // 使用 update 方法
if (cache != null) {
out.write(cache, 0, cache.length);
}
offSet += chunkSize; // 更新 offset
}
// 最后调用 doFinal 方法处理剩余数据
cache = cipher.doFinal(); // 不再传入数据
if (cache != null) {
out.write(cache, 0, cache.length);
}
out.close();
return out.toByteArray();
}
}
vue3 实现 RSA 加解密
依赖库:jsencrypt
import {JSEncrypt} from 'jsencrypt';
import type {RsaKeyPair} from "@/utils/axios";
//RSA 位数,这里要跟后端对应
// eslint-disable-next-line no-unused-vars
let bits = 1024;
//当前JSEncrypted对象
let thisKeyPair: any = {};
//生成密钥对(公钥和私钥)
function genKeyPair() {
let genKeyPair: RsaKeyPair = {
privateKey: '',
publicKey: ''
};
thisKeyPair = new JSEncrypt();
//获取私钥
genKeyPair.privateKey = thisKeyPair.getPrivateKey();
genKeyPair.privateKey = genKeyPair.privateKey.replace("-----BEGIN RSA PRIVATE KEY-----\n", "");
genKeyPair.privateKey = genKeyPair.privateKey.replace("\n-----END RSA PRIVATE KEY-----", "");
//获取公钥
genKeyPair.publicKey = thisKeyPair.getPublicKey();
genKeyPair.publicKey = genKeyPair.publicKey.replace("-----BEGIN PUBLIC KEY-----\n", "");
genKeyPair.publicKey = genKeyPair.publicKey.replace("\n-----END PUBLIC KEY-----", "");
return genKeyPair;
}
//公钥加密
function rsaEncrypt(plaintext: any, publicKey: any) {
if (plaintext instanceof Object) {
//1、JSON.stringify
plaintext = JSON.stringify(plaintext)
}
publicKey && thisKeyPair.setPublicKey(publicKey);
return thisKeyPair.encrypt(plaintext);
}
//私钥解密
function rsaDecrypt(ciphertext: any, privateKey: any) {
privateKey && thisKeyPair.setPrivateKey(privateKey);
let decString = thisKeyPair.decrypt(ciphertext);
if (decString.charAt(0) === "{" || decString.charAt(0) === "[") {
//JSON.parse
decString = JSON.parse(decString);
}
return decString;
}
export default {
genKeyPair,
rsaEncrypt,
rsaDecrypt
}
Java 加解密整体思路实现
ps:
- 利用 AOP 自定义注解实现对参数和响应的加解密
- 针对加解密进行封装一个公共的工具类:ApiSecurityUtil
AOP 环绕通知
import com.alibaba.fastjson2.JSON;
import com.yannqing.template.utils.ApiSecurityUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
@Aspect
@Component
public class SafetyAspect {
/**
* Pointcut 切入点
* 匹配com.yannqing.template.controller包下面的所有方法
*/
@Pointcut("execution(* com.yannqing.template.controller..*.*(..))")
public void safetyAspect() {
}
/**
* 环绕通知
*/
@Around(value = "safetyAspect()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
//request对象
HttpServletRequest request = attributes.getRequest();
//http请求方法 post get
String httpMethod = request.getMethod().toLowerCase();
//method方法
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
//method方法上面的注解
Annotation[] annotations = method.getAnnotations();
//方法的形参参数
Object[] args = pjp.getArgs();
//是否有@Decrypt
boolean hasDecrypt = false;
//是否有@Encrypt
boolean hasEncrypt = false;
for (Annotation annotation : annotations) {
if (annotation.annotationType() == Decrypt.class) {
hasDecrypt = true;
}
if (annotation.annotationType() == Encrypt.class) {
hasEncrypt = true;
}
}
//执行方法之前解密,且只拦截post请求
if ("post".equals(httpMethod) && hasDecrypt) {
//api解密
String decrypt = ApiSecurityUtil.decrypt();
//注:参数最好用Vo对象来接参,单用String来接,args有长度但获取为空,很奇怪不知道为什么
if(args.length > 0){
args[0] = JSON.parseObject(decrypt, args[0].getClass());
}
}
//执行并替换最新形参参数 PS:这里有一个需要注意的地方,method方法必须是要public修饰的才能设置值,private的设置不了
Object o = pjp.proceed(args);
//返回结果之前加密
if (hasEncrypt) {
//api加密,转json字符串并转成Object对象,设置到Result中并赋值给返回值o
o = ApiSecurityUtil.encrypt(o);
}
//返回
return o;
}
}
ApiSecurityUtil 封装
import com.alibaba.fastjson2.JSON;
import com.yannqing.template.common.Code;
import com.yannqing.template.domain.BaseResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* API接口 加解密工具类
*/
@Slf4j
public class ApiSecurityUtil {
/**
* API解密
*/
public static String decrypt(){
try {
//从RequestContextHolder中获取request对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//AES加密后的数据
// String data = getRequestBody(request);
String data = request.getParameter("data");
//后端RSA公钥加密后的AES的key
String aesKey = request.getParameter("aesKey");
//后端私钥解密的到AES的key
byte[] plaintext = RsaUtil.decryptByPrivateKey(Base64.decodeBase64(aesKey), RsaUtil.getPrivateKey());
aesKey = new String(plaintext);
//AES解密得到明文data数据
return AesUtil.decrypt(data, aesKey);
} catch (Throwable e) {
//输出到日志文件中
log.error(e.getMessage());
throw new RuntimeException("ApiSecurityUtil.decrypt:解密异常!");
}
}
/**
* API加密
*/
public static BaseResponse<Object> encrypt(Object object){
try {
//从RequestContextHolder中获取request对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
//前端公钥
String publicKey = request.getParameter("publicKey");
//随机获取AES的key,加密data数据
String key = AesUtil.getKey();
String dataString;
if(object instanceof String){
dataString = String.valueOf(object);
}else{
dataString = JSON.toJSONString(object);
}
//随机AES的key加密后的密文
String data = AesUtil.encrypt(dataString, key);
//用前端的公钥来解密AES的key,并转成Base64
String aesKey = Base64.encodeBase64String(RsaUtil.encryptByPublicKey(key.getBytes(), publicKey));
return ResultUtils.success(Code.SUCCESS, JSON.parse("{\"data\":\"" + data + "\",\"aesKey\":\"" + aesKey + "\"}"));
} catch (Throwable e) {
//输出到日志文件中
log.error(e.getMessage());
throw new RuntimeException("ApiSecurityUtil.encrypt:加密异常!");
}
}
}
Controller 中应用
import com.yannqing.template.aspect.Decrypt;
import com.yannqing.template.aspect.Encrypt;
import com.yannqing.template.common.Code;
import com.yannqing.template.domain.BaseResponse;
import com.yannqing.template.domain.dto.TestLoginDTO;
import com.yannqing.template.utils.ResultUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @description: 测试 controller
* @author: yannqing
* @create: 2024-10-29 10:22
* @from: <更多资料:yannqing.com>
**/
@Tag(name = "测试controller")
@RestController
@RequestMapping("/test")
public class TestController {
@Encrypt
@Decrypt
@Operation(summary = "测试加密接口 test1")
@PostMapping("/test1")
public BaseResponse<Object> test1(TestLoginDTO testLoginDTO) {
System.out.println(testLoginDTO);
return ResultUtils.success(Code.SUCCESS, testLoginDTO, "返回数据 ok");
}
}
Vue3 加解密整体思路实现
核心思想:在 axios interceptor 中进行加解密
依赖库:qs
Request Interceptor
// Add a request interceptor 请求拦截器
service.interceptors.request.use(
function (config) {
// 系统错误
if (!config.headers) {
throw new Error(
`Expected 'config' and 'config.headers' not to be undefined`,
);
}
if (!config.params) {
config.params = {}; // 初始化 params 为对象
}
console.log("config.data", config.data)
//获取前端RSA公钥密码、AES的key,并放到window
if (config.method === "post" && config.data) {
//1. 获取前端 RSA 公钥和私钥
let genKeyPair: RsaKeyPair = rsa.genKeyPair();
localStorage.setItem("jsPublicKey", genKeyPair.publicKey)
localStorage.setItem("jsPrivateKey", genKeyPair.privateKey)
// window.jsPublicKey = genKeyPair.publicKey;
// window.jsPrivateKey = genKeyPair.privateKey;
console.log("================前端 RSA 公钥和私钥生成成功!")
//2. 获取前端 AES Key
let aesKey = aes.genKey()
console.log("================前端 AES Key 生成成功!")
//3. 获取后端 RSA 公钥
let backendPublicKey: string | null = localStorage.getItem(BACKEND_PUBLIC_KEY);
if (!backendPublicKey) {
backendPublicKey = getPublicKey();
}
console.log("后端 RSA 公钥", backendPublicKey)
console.log("================获取后端 RSA 公钥成功!")
//4. 使用前端 AES 加密请求数据
const dataRes = aes.encrypt(config.data, aesKey);
console.log("加密 data", dataRes)
//5. 使用后端 RSA 公钥加密前端 AES,并放到请求头
const aesKeyRes = rsa.rsaEncrypt(aesKey, backendPublicKey);
console.log("加密 aes", aesKeyRes);
config.params['aesKey'] = aesKeyRes;
let dataVo = {
data: dataRes,
aesKey: aesKeyRes,//后端RSA公钥加密后的AES的key
publicKey: localStorage.getItem("jsPublicKey")//前端公钥,
};
config.data = qs.stringify(dataVo);
}
return config;
},
function (error) {
// Do something with request error
return Promise.reject(error);
},
);
Response Interceptor
interface BaseResponse<Object> {
code: number,
data: Object,
msg: string,
}
interface EncryptData {
data: string,
aesKey: string,
}
// Add a response interceptor 响应拦截器
service.interceptors.response.use(
function (response) {
console.log("response data", response.data)
// 加密数据
if (JSON.parse(response.data).data.data) {
const responseData: BaseResponse<EncryptData> = JSON.parse(response.data)
console.log("responseData", responseData)
const decryptAes = rsa.rsaDecrypt(responseData.data.aesKey, localStorage.getItem("jsPrivateKey"))
console.log("解密 AES :", decryptAes)
const decryptData = aes.decrypt(responseData.data.data, decryptAes);
console.log("解密 data: ", decryptData)
return decryptData;
} else {
// 未加密
const responseData: BaseResponse<any> = JSON.parse(response.data)
console.log("未加密 返回数据:", responseData)
return responseData
}
},
function (error) {
console.log("error", error);
return Promise.reject(error);
},
);
加解密失败可能的原因
- 前端请求的 content-type 要设置为
application/x-www-form-urlencoded; charset=UTF-8
- 后端 controller 中接收参数不能用 @RequestBody 注解,Spring 会将前端请求参数识别为 json 进行解析,导致报错,可以用 String 来接收,查看是否可以接收到对应的参数
- 每次重启前后端,生成的 RSA 密钥对都是新的,一定要保证解密时候的私钥是和加密的公钥是匹配的!!