AES + RSA 实现前后端数据加密传输

2024 年 10 月 31 日 星期四(已编辑)
/ , , , ,
21
摘要
AES 和 RSA 的基本介绍以及如何结合使用两者实现前后端传输数据过程中进行加密

AES + RSA 实现前后端数据加密传输

参考:https://www.cnblogs.com/privateLogs/p/17888711.html

环境:

  • 后端 spring boot
  • 前端 vue3 + axios

对称加密:加密和解密用同一组密钥。AES(一组 key,进行加密和解密)

非对称加密:加密和解密不用同一组密钥。RSA(公钥加密,私钥解密)

核心思想

后端:

  1. 后端RSA:公钥public1,私钥private1
  2. 后端AES:aes1
  3. 后端加密数据:data1

前端:

  1. 前端RSA:公钥public2,私钥private2
  2. 前端AES:aes2
  3. 前端加密数据:data2

步骤:

  1. 后端生成public1,private1
  2. 前端请求后端,得到public1
  3. 前端生成public2,private2,aes2
  4. 前端用 aes2 加密 data2,用 public1 加密 aes2
  5. 前端请求数据:加密data2,加密aes2,public2
  6. 后端使用 private1 解密 aes2
  7. 后端使用 aes2 解密 data2
  8. 后端生成 aes1
  9. 后端用 aes1 加密响应 data1,使用 public2 对 aes1 加密
  10. 后端响应数据:加密 data1,加密 aes1
  11. 前端使用 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);
  },
);

加解密失败可能的原因

  1. 前端请求的 content-type 要设置为 application/x-www-form-urlencoded; charset=UTF-8
  2. 后端 controller 中接收参数不能用 @RequestBody 注解,Spring 会将前端请求参数识别为 json 进行解析,导致报错,可以用 String 来接收,查看是否可以接收到对应的参数
  3. 每次重启前后端,生成的 RSA 密钥对都是新的,一定要保证解密时候的私钥是和加密的公钥是匹配的!!
  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...