springboot+vue接口加密:RSA+AES

1. 整体预览

  • 整体思想为:

先使用AES对数据进行加密,再使用RSA对AES密钥进行加密。

  • 至于为什么要使用这种方式呢?总结起来大致就是又安全速度又快。
1. RSA算法复杂,比较耗时,但比较安全
2. AES密钥固定,双方使用同一密钥,但速度快效率高
3. 所以用AES密钥加密数据,RSA加密AES密钥,形成混合加密
  • 至于RSA和AES是什么?文章篇幅有限,还请大家到网上自行了解。

  • 接下来是实现加密的具体流程。

  • 请求:
  1. 客户端发起请求时,客户端使用随机生成的AES密钥对数据进行加密
  2. 使用服务器的 公钥 对AES密钥进行加密
  3. 加密后的数据和AES密钥 作为请求数据发送至服务器。
  4. 服务器收到请求后,使用服务器的 私钥 对加密过的AES密钥进行解密
  5. 再使用获取到的AES密钥对加密的数据进行解密。
  6. 以此保证客户端发送的数据只能被服务器解密进行处理。
  • 响应:
  1. 服务器处理完数据后,将响应数据先使用随机生成(新)的AES密钥进行加密
  2. 使用服务器的 私钥 对AES密钥进行签名(签名算法为[ MD5withRSA ])
  3. 加密后的响应数据、AES密钥、签名 作为响应数据发送至客户端。
  4. 客户端收到响应后,先使用服务器的 公钥 对签名过的AES密钥进行验证
  5. 验签成功后再使用的AES密钥对加密的响应数据进行解密。
  6. 以此保证客户端收到的响应数据为合法服务器返回的。

2. 加密方式

  • 加密算法: AES
  • KEY长度: 16 * 8
  • 加密模式: ECB
  • 数据填充方式: PKCS5Padding
  • 加密算法: RSA
  • RSA位数: 2048
  • 加密模式: ECB
  • 数据填充方式: PKCS1Padding

3. 例

  • 需要加密的数据: {"staffUid": "zs"}
  • 生成的AES密钥: 1@HLKMUCHkywdrel
  • 数据加密后: 1qQL/+ufmvg124o5lc/IvzNVKsJniWomAaTrwWLX/4E=
  • 服务器公钥:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnYnyTKWgBrrPeP/5D0w8Ts5aSiJKOgZ2TV9WkTDLwnE4nHpKxkE80uEP0jocJYboZ2wY+M9w4U01RBc7V5uqI4w46EoD0t3oBWkymKYsOfs0FLdPsqeGc7wUBkysJ3oGJcv3DRBXUrnJo3LJbxpOW/34CdGsLpfND6rAQFnR6oQNWUUskZYobsbhNSO7MGp0FSZNnXdz9ahS2QAHQWkycBZfqx6iLqA701LTsBrA4cAWTMxmlKrCSUCwZb85hrK7fCNkS8NMqpMLuN466TQgfZ/umFD38LwdiW2k/5glxNkhGsrjFmPzEnGqYhhIG1BJ3npdX/dhU69D3r91g8Hf4wIDAQAB
  • 使用公钥加密后的AES密钥:
ihPxFWDmLPDw5VUXu4ClbFBZxy92ZKhp4E8ypl7uM/9gWJpKPMcMPk1GERPKmzD24Rx06v3ROuo5Dj1Lryc1XVjvvSC9Z6Jq7fmCtMUjQJW/7PdXgO9PQmId/3J2q5NtWPutyyuyTAz5J+tDPGKUlzGvO5vcxQVJXj4O3kBYZqZu8yVqbtx23zeXclQPmtmJYxNaZO84eJb1HymlngbmDKgDtH0+UyRM9fgksDDBbI6gfeRvSOdUmlD/oTqnnS0AFzucjnnNFeugeoRlE1sXnKuv47ePpJDLzgX+XHZjwhWhMLmNLZscE0IL1Ue4JUdRmzKc3Xb3SUu+/b6zBiHl6w==
  • 发送的请求数据:
data: 1qQL/+ufmvg124o5lc/IvzNVKsJniWomAaTrwWLX/4E=
aesKey: ihPxFWDmLPDw5VUXu4ClbFBZxy92ZKhp4E8ypl7uM/9gWJpKPMcMPk1GERPKmzD24Rx06v3ROuo5Dj1Lryc1XVjvvSC9Z6Jq7fmCtMUjQJW/7PdXgO9PQmId/3J2q5NtWPutyyuyTAz5J+tDPGKUlzGvO5vcxQVJXj4O3kBYZqZu8yVqbtx23zeXclQPmtmJYxNaZO84eJb1HymlngbmDKgDtH0+UyRM9fgksDDBbI6gfeRvSOdUmlD/oTqnnS0AFzucjnnNFeugeoRlE1sXnKuv47ePpJDLzgX+XHZjwhWhMLmNLZscE0IL1Ue4JUdRmzKc3Xb3SUu+/b6zBiHl6w==
  • 响应结果如下:
{
    "code": 200,
    "message": "操作成功",
    "data": {
	"data": "zKpHLUQ9NtvRizSUwdEtuM5pMOWf8qBGmGuW1ZYqGeQMw/00i4wwhXMCS0MoabdJ4zP6ETdZip02xq9sb/4EtRvLFWEqoVOs4MXtFV8QYiWsMxw1jbmRQmWCc8WBlqtQVkLyQfdQw51LIapbLtfrZGXhryqSHZhil6RUHU4RuXqwUPBMzlsobquhxKMNePJ6hGcEi5Z+JIcAeUNVWCP9hrfNPl/zYNrf4WWvmCW/tnBS2ffg34Unqho+iywEW+46bUfbgnE34LgeZqCNMG7MfyNY+DHueaWcwcPekNzzbdmawWUfs4miTUkh7Uz7Fm7+AdrmM/ZCgIqwene1LpYZVaJyHgLwlLX31gzXVeQxYpdshnbz+x6Rmf81xS+gysVrQywXVY13/QWz+mb9jmPqcS04vHf9sbA0pZ7UjUMVe+3mXK4mUdL11eq+Ti+ZvqnEIYKkOxKrOJWoQ1mKT86cMoYcS18Fu8+oTgRch2S1QZrEVApnoHBvjJXsgsl4PRIHWZZEPGhFAohK0fXQXr7JQQLee8ALpxVEd1RyFOevoBnO/Dt5MeKsmKNBK7HMQpxB",
	"aesKey": "U85n3MtPMlmPTwLs",
	"sign": "F6mQbV40E9SbuV/m+jIH+VDCqtxFOhzO10XLCGZoZ2b8We5a1BSXPum8S+7oAu/JaqfYi3fPFjzLHCVu+uWhSMhch3m1IRFOpZp7Pa04AawbUv0MuyrtyBqgNjLxJna5FhTX4NbOc4VaT1gvLO3wi4KWuZ5Ymp1ALWDW1QrmJkYzKasp6lsVyovYuj6GWESykdlmskzdI6iECSXtRatb3NpIe2vc9By8sXd8VytbcNlTHQHcidnuQNprYexp53IpbMRNma9zpqM+iqzX+PWUYlaSMLxeUEskLT3h/V4YhMj2vorrf1HM+nyGxO2gR+sVQnb8sy7caJy8c/2m12AlwA=="
    }
}
  • 响应解密后:
{
   "code": 200,
   "message": "操作成功",
   "data": {
   	"id": 1,
   	"staffUid": "zs",
   	"staffPassword": null,
   	"staffName": "张三",
   	"staffCardId": "123",
   	"staffLoginName": "zszs",
   	"staffEmail": "[email protected]",
   	"staffPhone": "13696365412",
   	"staffAdmin": 1,
   	"staffStatus": 0,
   	"staffDepartment": "IT",
   	"staffCode": "qwer",
   	"remark": null,
   	"createdBy": "system",
   	"createdTime": "2022-10-27 14:14:50",
   	"updatedBy": "zs",
   	"updatedTime": "2022-11-21 17:54:22",
   	"status": 0
   }
}

4. 代码

前端

  • 在何处使用?某个vue文件中,将参数通过方法加密后传入请求,待请求结束后,对响应数据使用对应的方法验签解密。
// 测试用例 此处没有逻辑关系
loginForm: {
  staffUid: ''
}
// get请求和post请求不同,get为普通传参,post需要使用表单传参,至于原因后面会解释
// get举例
getStaffInfo(resEncryptionForGet(this.loginForm)).then((response) => {
  console.log(resDecryption(response.data))
}).catch(() => {

})
// post举例
testLogin(resEncryption({token: this.loginForm.staffUid})).then((response) => {
  console.log(resDecryption(response.data))
}).catch(() => {

})
  • 请求长什么样子?某个js文件中,通过封装的request向后端发送axios请求。此处的request与普通的并无二致,包括axios的创建,request和response拦截器,对请求及响应进行统一处理,本文不再列出。
import request from "@/utils/request";
export function getStaffInfo(params) {
  return request({
    url: '/api/staff/getLoginStaffInfo',
    method: 'get',
    params: params
  })
}
export function testLogin(params) {
  return request({
    url: '/api/staff/login',
    method: 'post',
    data: params,
    // 使用表单传参大致是需要设置headers的
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  })
}
  • 加解密方法长什么样子?某个js文件中,包括随机生成AES的密钥、AES加解密、公钥加密(和验签)等方法。较为复杂,还请读者自行消化。
import {b64tohex, KJUR} from 'jsrsasign' // base64转16进制 验签所需插件
import CryptoJS from 'crypto-js' // AES
import forge from 'node-forge' // RSA
import request from "./request";
import store from "../store";

// 客户端(前端)需要获取服务端(后端)的公钥,此处使用接口获取,可以不通过接口获取
request({
  url: '/api/staff/getPublicKey',
  method: 'get'
}).then(result => {
  // console.log(""+result.data)
  store.state.user.serverPublicKey = result.data;
})

// 生成AES密钥
function getmm(num = 16) {
  var amm = ['!', '@', '#', '$', '%', '&', '*', '(', ')', '_', 1, 2, 3, 4, 5, 6, 7, 8, 9]
  var tmp = Math.floor(Math.random() * num)
  var s = tmp
  s = s + amm[tmp]
  for (let i = 0; i < Math.floor(num / 2) - 1; i++) {
    tmp = Math.floor(Math.random() * 26)
    s = s + String.fromCharCode(65 + tmp)
  }
  for (let i = 0; i < (num - Math.floor(num / 2) - 1); i++) {
    tmp = Math.floor(Math.random() * 26)
    s = s + String.fromCharCode(97 + tmp)
  }
  return s
}

// 生成客户端RSA公钥和私钥 需要双向加密时需要,将在文末讨论
// export function jsrsasignFn() {
//   var rsaKeypair = jsrsasign.KEYUTIL.generateKeypair('RSA', 2048)
//   var private1 = jsrsasign.KEYUTIL.getPEM(rsaKeypair.prvKeyObj, 'PKCS1PRV')
//   var public1 = jsrsasign.KEYUTIL.getPEM(rsaKeypair.pubKeyObj)
//   let a = {
//     'privateKey': private1.substring(31, private1.length - 31).replace(/\r\n/g, ''),
//     'publicKey': public1.substring(28, public1.length - 28).replace(/\r\n/g, '')
//   }
//   console.log(a)
//   return a
// }

// AES 加密  data:要加密解密的数据,AES_KEY:密钥,
function encrypt(data, AES_KEY) {
  const key = CryptoJS.enc.Utf8.parse(AES_KEY)
  const encrypted = CryptoJS.AES.encrypt(data, key, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7
  })
  // console.log("加密数据:" + data)
  // console.log("数据加密后:" + encrypted.toString())
  return encrypted.toString()
}

// 使用服务端的公钥加密AES密钥
function pubencrypt(aeskey, pubencryptKey) {
  // console.log("aeskey:" + aeskey)
  // console.log("服务器公钥:" + pubencryptKey)
  // 此处的前后缀不能改变
  const publicKeyAll = '-----BEGIN PUBLIC KEY-----\n' + pubencryptKey + '\n-----END PUBLIC KEY-----'
  var publicKey = forge.pki.publicKeyFromPem(publicKeyAll)
  var buffer = forge.util.createBuffer(aeskey, 'utf8')
  var bytes = buffer.getBytes()
  // console.log("公钥加密后的AES密钥:" + a)
  return forge.util.encode64(publicKey.encrypt(bytes, 'RSAES-PKCS1-V1_5'))
}

// post请求参数加密
export function resEncryption(data) {
  // 生成随机密钥(key)
  var key = getmm()
  // 密钥 加密 数据(将json转成字符串再进行加密)
  var newData = encrypt(JSON.stringify(data), key)
  // console.log("--------------------"+newData)
  // 公钥 加密 密钥(key组成)
  var aesKey = pubencrypt(key, store.state.user.serverPublicKey)
  // console.log(aesKey)
  // 返回数据
  let formData = new FormData();
  formData.append('data', newData)
  formData.append('aesKey', aesKey)
  return formData
}

// get请求参数加密
export function resEncryptionForGet(data) {
  // 生成随机密钥(key)
  var key = getmm()
  // 密钥 加密 数据(将json转成字符串再进行加密)
  var newData = encrypt(JSON.stringify(data), key)
  console.log("--------------------"+newData)
  // 公钥 加密 密钥(key组成)
  var aesKey = pubencrypt(key, store.state.user.serverPublicKey)
  // console.log(aesKey)
  // 返回数据
  return {
    'data': newData,
    'aesKey': aesKey
  }
}

// 使用服务端的公钥验签AES密钥
function pubverify(aeskey, pubencryptKey, sign) {
  // console.log("aeskey:" + aeskey)
  // console.log("服务器公钥:" + pubencryptKey)
  const publicKeyAll = '-----BEGIN PUBLIC KEY-----\n' + pubencryptKey + '\n-----END PUBLIC KEY-----'
  try {
    let sig =  new KJUR.crypto.Signature({alg: "MD5withRSA"});
    sig.init(publicKeyAll)
    sig.updateString(aeskey)
    return sig.verify(b64tohex(sign))
  } catch (e) {
    console.log(e)
  }
}

// AES 加密  data:要加密解密的数据,AES_KEY:密钥,
function aesDecrypt(data, AES_KEY) {
  // console.log("---------------------开始解密AES")
  const decrypted = CryptoJS.AES.decrypt(data.toString(), CryptoJS.enc.Utf8.parse(AES_KEY), {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7
  })
  // console.log("数据:" + data.toString())
  // console.log("解密后:" + decrypted.toString(CryptoJS.enc.Utf8))
  return decrypted.toString(CryptoJS.enc.Utf8)
}

// 响应解密
export function resDecryption(data) {
  // 使用公钥先验签aesKey
  let verifyResult = pubverify(data.aesKey, store.state.user.serverPublicKey, data.sign);
  if (!verifyResult) {
    this.$message({
      type: 'warning',
      message: '响应数据不合法!'
    });
    return ;
  }
  console.log(verifyResult)
  // 使用aesKey解密数据
  return aesDecrypt(data.data, data.aesKey);
}

后端

  • 所需哪些依赖?包括接口加密与base64转换的依赖。


    org.bouncycastle
    bcpkix-jdk15on
    1.56



    org.apache.directory.studio
    org.apache.commons.codec
    1.8
  • 如何使用加解密?定义两个用于加解密的注解,使用时在需要加解密的接口方法上进行注解。
// Encrypt.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypt {
}

// Decrypt.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Decrypt {
}
  • AES与RSA如何实现加解密?使用定义的静态工具类,也请读者自行消化。网上关于后端部分的内容较多,作者在此处为直接借鉴。
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 Integer 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中,导致内存缓便被耗尽
     */
    private static final BouncyCastleProvider PROVIDER = new BouncyCastleProvider();

    static {
        key = getKey();
    }

    /**
     * 获取key
     */
    public static String getKey() {
        StringBuilder uid = new StringBuilder();
        //产生16位的强随机数
        Random rd = new SecureRandom();
        for (int i = 0; i < KEY_LENGTH / 8; i++) {
            //产生0-2的3位随机数
            int type = rd.nextInt(3);
            switch (type) {
                case 0:
                    //0-9的随机数
                    uid.append(rd.nextInt(10));
                    break;
                case 1:
                    //ASCII在65-90之间为大写,获取大写随机
                    uid.append((char) (rd.nextInt(25) + 65));
                    break;
                case 2:
                    //ASCII在97-122之间为小写,获取小写随机
                    uid.append((char) (rd.nextInt(25) + 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);
        // byte[] a = new String(decodeBase64).getBytes(StandardCharsets.UTF_8);
        //设置Cipher对象
        Cipher cipher = Cipher.getInstance(ALGORITHMS,PROVIDER);
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(StandardCharsets.UTF_8), KEY_ALGORITHM));
        //调用doFinal解密
        return new String(cipher.doFinal(decodeBase64));
    }

}
@Slf4j
public class RSAUtil {

    /**
     * 加密算法RSA
     */
    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 = 245;

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

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

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

    static {
        try {
            genKeyPair.putAll(genKeyPair());
        } catch (Exception e) {
            // 输出到日志文件中
            log.error(e.getMessage());
            // System.err.println(e.getMessage());
        }
    }

    /**
     * 生成密钥对(公钥和私钥)
     */
    private static Map genKeyPair() throws Exception {
        log.info("-------------------开始生成密钥对");
        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 keyMap = new HashMap<>(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);
    }

    /**
     * 私钥加密
     * @param data       源数据
     * @param privateKey 私钥(BASE64编码)
     */
    public static byte[] encryptByPrivateKey(byte[] data, String privateKey) throws Exception {
        Key privateK = KeyFactory.getInstance(KEY_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));
        Cipher cipher = Cipher.getInstance(ALGORITHMS);
        cipher.init(Cipher.ENCRYPT_MODE, privateK);
        return encryptAndDecryptOfSubsection(data, cipher, MAX_ENCRYPT_BLOCK);
    }

    /**
     * 公钥解密
     * @param encryptedData 已加密数据
     * @param publicKey     公钥(BASE64编码)
     */
    public static byte[] decryptByPublicKey(byte[] encryptedData, String publicKey) throws Exception {
        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(encryptedData, 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;
        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();
    }

    /**
     * 用私钥对信息生成数字签名
     * @param data       已加密数据
     * @param privateKey 私钥(BASE64编码)
     */
    public static String sign(byte[] data, String privateKey) throws Exception {
        byte[] keyBytes = Base64.decodeBase64(privateKey);
        PrivateKey privateK = KeyFactory.getInstance(KEY_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
        Signature signature = Signature.getInstance("MD5withRSA");
        signature.initSign(privateK);
        signature.update(data);
        return Base64.encodeBase64String(signature.sign());
    }

    /**
     * 校验数字签名
     * @param data      已加密数据
     * @param publicKey 公钥(BASE64编码)
     * @param sign      数字签名
     */
    public static boolean verify(byte[] data, String publicKey, String sign) throws Exception {
        byte[] keyBytes = Base64.decodeBase64(publicKey);
        PublicKey publicK = KeyFactory.getInstance(KEY_ALGORITHM).generatePublic(new X509EncodedKeySpec(keyBytes));
        Signature signature = Signature.getInstance("MD5withRSA");
        signature.initVerify(publicK);
        signature.update(data);
        return signature.verify(Base64.decodeBase64(sign));
    }

}
  • 加解密具体在哪里实现?通过spring特性aop的环绕通知,找到切入点下拥有前面定义的加解密注解的接口,对数据进行处理。
@Slf4j
@Aspect
@Component
public class SafetyAspect {

    /**
     * Pointcut 切入点
     * 匹配
     * cn.huanzi.qch.baseadmin.sys.*.controller、
     * cn.huanzi.qch.baseadmin.*.controller包下面的所有方法
     */
    @Pointcut(value = "execution(public * com.sy.order.modules.api.controller..*.*(..))")
    public void safetyAspect() {}

    /**
     * 环绕通知
     */
    @Around(value = "safetyAspect()")
    public Object around(ProceedingJoinPoint pjp) {
        try {
            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) {
                hasDecrypt = annotation.annotationType() == Decrypt.class;
                hasEncrypt = annotation.annotationType() == Encrypt.class;
            }
            ObjectMapper mapper = new ObjectMapper();
            // jackson 序列化和反序列化 date处理
            mapper.setDateFormat( new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
            // 执行方法之前解密
            if (hasDecrypt) {
                // 此处填坑,后端是用如下的方式获取前端所传参数的,get请求没有问题,但post请求获取body参数时就涉及到request流只能读一次的问题,为了能获取到参数规定前端post请求时使用表单传参,有要求的可自行修改。
                // AES加密后的数据
                String dataTmp = request.getParameter("data");
                log.debug("前端数据:[{}]", dataTmp);
                // 后端RSA公钥加密后的AES的key
                String aesKey = request.getParameter("aesKey");
                log.debug("AES的加密key:[{}]", aesKey);
                // 后端私钥解密的到AES的key
                byte[] plaintext = RSAUtil.decryptByPrivateKey(Base64.decodeBase64(aesKey), RSAUtil.getPrivateKey());
                aesKey = new String(plaintext);
                log.debug("解密出来的AES的key:[{}]", aesKey);
                // AES解密得到明文data数据
                String decrypt = AESUtil.decrypt(dataTmp, aesKey);
                log.debug("解密出来的data数据:[{}]", decrypt);
                // 设置到方法的形参中,目前只能设置只有一个参数的情况
                mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
                if(args.length > 0){
                    args[0] = mapper.readValue(decrypt, args[0].getClass());
                }
            }
            // 执行并替换最新形参参数 method方法要public修饰的才能设置值
            Object o = pjp.proceed(args);

            // 返回结果之前加签名
            if (hasEncrypt) {
                mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
                // 每次响应之前随机获取AES的key,加密data数据
                String key = AESUtil.getKey();
                log.debug("本次响应加密前AES key:[{}]", key);
                String dataString = mapper.writeValueAsString(o);
                log.debug("需要加密的data数据:[{}]", dataString);
                String data = AESUtil.encrypt(dataString, key);
                log.debug("加密后的data数据:[{}]", data);
                // 对key 用私钥加签
                String sign = RSAUtil.sign(key.getBytes(), RSAUtil.getPrivateKey());
                // 转json字符串并转成Object对象,并赋值给返回值o
                o = CommonResult.success(mapper.readValue("{"data":"" + data + "","aesKey":"" + key + "","sign":"" + sign + ""}", Object.class));
            }
            return o;
        } catch (Throwable e) {
            log.error(e.getMessage());
            // CommonResult为后端统一返回类
            return CommonResult.failed(ResultCode.REQUEST_FAIL, e.getMessage());
        }
    }
}

5. 注意

  • 客户端post请求数据统一为FormData格式
  • 客户端需要先获取服务器公钥,本文使用接口获取
  • 数据传输时使用base64编码,前端部分插件数据处理完时即为base64,需看情况处理

你可能感兴趣的:(spring,boot,后端)