前端实现RSA和AES中遇到的问题

356 阅读6分钟

首先我们先了解一下什么是RSA和AES

整个前后端交互的通信做了一个加密的操作,只要加密的key不泄露,别人得到你的数据也没用,问题是如何保证key不泄露呢?服务端的安全性较高,可以存储在数据库中或者配置文件中,毕竟在我们自己的服务器上,最危险的其实就时前端了,app还好,可以打包,但是要防止反编译等等问题。

如果是webapp则可以依赖于js加密来实现,下面我给大家介绍一种动态获取加密key的方式,只不过实现起来比较复杂,我们不上代码,只讲思路:

加密算法有对称加密和非对称加密,AES是对称加密,RSA是非对称加密。之所以用AES加密数据是因为效率高,RSA运行速度慢,可以用于签名操作。

我们可以用这2种算法互补,来保证安全性,用RSA来加密传输AES的密钥,用AES来加密数据,两者相互结合,优势互补。

其实大家理解了HTTPS的原理的话对于下面的内容应该是一看就懂的,HTTPS比HTTP慢的原因都是因为需要让客户端与服务器端安全地协商出一个对称加密算法。剩下的就是通信时双方使用这个对称加密算法进行加密解密。

  1. 客户端启动,发送请求到服务端,服务端用RSA算法生成一对公钥和私钥,我们简称为pubkey1,prikey1,将公钥pubkey1返回给客户端。
  2. 客户端拿到服务端返回的公钥pubkey1后,自己用RSA算法生成一对公钥和私钥,我们简称为pubkey2,prikey2,并将公钥pubkey2通过公钥pubkey1加密,加密之后传输给服务端。
  3. 此时服务端收到客户端传输的密文,用私钥prikey1进行解密,因为数据是用公钥pubkey1加密的,通过解密就可以得到客户端生成的公钥pubkey2
  4. 然后自己在生成对称加密,也就是我们的AES,其实也就是相对于我们配置中的那个16的长度的加密key,生成了这个key之后我们就用公钥pubkey2进行加密,返回给客户端,因为只有客户端有pubkey2对应的私钥prikey2,只有客户端才能解密,客户端得到数据之后,用prikey2进行解密操作,得到AES的加密key,最后就用加密key进行数据传输的加密,至此整个流程结束。

在js文件中使用pinia仓库

Pinia在vue文件夹中是挂载到setup函数上面的,但是js文件夹内中直接使用就会报错,说在调用的时候显示没有激活pinia image.png 我的解决方案是这样的,在js文件或者ts文件中直接引入pinia但是,在实例化的时候在函数内部创建即可 下面是我在拦截器的js文件中使用pinia: image.png

这样可能会导致每一次发送请求的时候,这个pinia的实力都会被创建出来,导致性能浪费。

在每一次请求之前都需要获取服务端生成的pubKey

因为这个pubKey是自动生成的,不需要用户手动生成,所以我们需要在请求拦截器中进行判断,然后自动请求获取后端生成的pubKey。又不能每一次请求的时候都需要请求这个pubKey,所以我把第一次请求回来的PubKey放在了pinia中,这样就可以实现了多余的请求。 image.png

重新配置axios

因为每一次请求之前都需要请求一个pubKey但是这个请求也会走请求拦截器,如果直接添加就会出现死循环,导致电脑崩掉。为了解决这个问题我们可以在请求的位置重新创建一个axios的实例,利用这个实例发送请求,这样就不会走拦截器,就可以完善上面的问题。

image.png

image.png

最终生成的AESKEY的时效问题

....

最后附上完整代码,提供给大家参考:

  1. api文件夹下的enctyption文件(对axios进行二次封装)
import axios from "axios";

import request from "@/utils/request";

// 配置基地址
const http = axios.create({
  baseURL: "************", // 任何的请求都会被拼上api
  timeout: 30000,
});

/***
 * 获取公钥
 *
 * @returns 服务端穿的的公钥1
 */
export function GetPublicKey() {
  return http({
    url: "/GetPublicKey",
    method: "POST",
  });
}

/***
 *获取后端返回的密文
 *
 * @param {object} 加密的私钥2和唯一标识
 */
export function GetCipherText(data) {
  return http({
    url: "/PushPubKey",
    method: "POST",
    data,
  });
}

/***
 *发送加密数据给后端
 *
 * @param {object} 第一个参数是加密数据,第二个数据是唯一标识
 * @returns {string} 加密数据
 */
export function DataEncrtpt(data) {
  return request({
    method: "POST",
    url: "/postData",
    data,
  });
}

  1. utils文件夹下的enctyption(用来封装这部分的逻辑函数)
import CryptoJS from "crypto-js";
import { JSEncrypt } from "encryptlong";
import { GetPublicKey, GetCipherText, DataEncrtpt } from "../api/encryption";
import { useUserStore } from "../stores/index";

/**
 * RSA 公钥加密
 *
 * @param content 待加密数据
 * @param publicKey 公钥
 * @returns {string} 加密结果
 */
export function rsaEncrypt(content, publicKey) {
  var encrypt = new JSEncrypt();
  encrypt.setPublicKey(publicKey);
  return encrypt.encryptLong(content);
}

/**
 * RSA 私钥解密
 *
 * @param content 待解密数据
 * @param privateKey 私钥
 * @returns {string} 解密结果
 */
export function rsaDecrypt(content, privateKey) {
  var encrypt = new JSEncrypt();
  encrypt.setPrivateKey(privateKey);
  return encrypt.decrypt(content);
}

/**
 * AES加密
 *
 * @param content 待加密的内容
 * @param secretKey 密钥
 * @param iv 初始向量
 * @returns {string} 加密结果
 */
export function aesEncrypt(content, secretKey, iv) {
  return CryptoJS.AES.encrypt(content, CryptoJS.enc.Utf8.parse(secretKey), {
    iv: CryptoJS.enc.Utf8.parse(iv),
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  }).toString();
}

/**
 * AES解密
 *
 * @param content 待解密的内容
 * @param secretKey 密钥
 * @param iv 初始向量
 * @returns {string} 解密结果
 */
export function aesDecrypt(content, secretKey, iv) {
  return CryptoJS.AES.decrypt(content, CryptoJS.enc.Utf8.parse(secretKey), {
    iv: CryptoJS.enc.Utf8.parse(iv),
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  }).toString(CryptoJS.enc.Utf8);
}

export const Go = async () => {
  const store = useUserStore();
  store.timeout = false;
  const rsa = new JSEncrypt();
  const { pubKey, uniqueId } = await GetPublicKey(); // pubKey公钥1
  debugger;
  store.uniqueid = uniqueId;
  const pubKey2 = rsa.getKey().getPublicKey(); // 客户端公钥
  const prikey2 = rsa.getKey().getPrivateKey(); // 客户端私钥
  const encPubKey2 = rsaEncrypt(pubKey2, pubKey); // 加密客户端公钥
  const datas = { data: { encPubKey2, uniqueId } };
  const { encKey2, encIV2 } = await GetCipherText(datas);
  store.encIv2 = rsaDecrypt(encIV2, prikey2);
  store.AESKEY = rsaDecrypt(encKey2, prikey2);
  console.log(store.AESKEY);
};

export const test = async (val) => {
  const store = useUserStore();
  console.log(store.AESKEY);
  console.log(store.uniqueid);
  console.log(store.encIv2);
  const datas = {
    data: {
      mobile: aesEncrypt(val, store.AESKEY, store.encIv2),
      uniqueId: store.uniqueid,
    },
  };
  try {
    const res = await DataEncrtpt(datas);
    console.log(
      "解密后的数据:",
      aesDecrypt(res.encData, store.AESKEY, store.encIv2)
    );
  } catch (error) {
    store.timeout = true;
    ElMessage({
      message: "秘钥过期,为您自动重试",
      type: "warning",
    });
  }
};

  1. 拦截器
import axios from "axios";
import { useUserStore } from "../stores/index";
import { Go } from "./encryption";

// 配置基地址
const request = axios.create({
  baseURL: "****************", // 任何的请求都会被拼上api
  timeout: 30000,
});
// 添加请求拦截器
request.interceptors.request.use(
  function (config) {
    const store = useUserStore();
    if (store.AESKEY) {
      Go();
    }
    return config;
  },
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  }
);

// 添加响应拦截器
request.interceptors.response.use(
  function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    // console.log(response);
    return response.data.data;
  },
  function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error);
  }
);

export default request;

4.pinia仓库的代码就不能给大家展示了

RSA+AES.jpg

RSA+AES加密流程图.jpg