1. 项目背景(需求)

为了保证数据传输的安全性,利用AES+RSA混合加密,配合后端实现数据交互加密

项目环境:vue + axios

2. 加密过程(流程)

请输入图片描述

请输入图片描述

3. 实现过程(代码)

AES对称加密我们采用 CryptoJS,AES加密支持AES-128、AES-192和AES-256 (AES传送门)

RSA非对称加密我们采用JSEncrypt,(RSA传送门)

第一步:npm安装两个库

npm i crypto-js jsencrypt

第二步:新建encrypt.js,封装需要用的方法

import CryptoJS from 'crypto-js'
import { JSEncrypt } from 'jsencrypt'

/**
 * 递归自然排序: key + value
 * sortObjFunc: 排序方法
 * isArraysFunc: 判断是否array
 * isObjectFunc: 判断是否object
 * isHasValues: 判断空值,null,undefined
 * 排序前: {"aaa":"111","bbb":"222","list_1":[],"list":["3","13"],"map":{"b":"2","c":"3"}}
 * 排序后: aaa111bbb222list423.852313list_1mapb2c3
 */
export const signUtil = {
  sortObjFunc: function (plaintext) {
    let signStr = ''
    let keyList = []
    for (const key in plaintext) { keyList.push(key) }
    if (keyList.length) { keyList.sort() }

    const len = keyList.length
    for (let i= 0; i < len; i++) {
      let value = plaintext[keyList[i]]
      if (value && signUtil.isHasValues(value)) {
        if (signUtil.isArraysFunc(value) || signUtil.isObjectFunc(value)) {
          value = signUtil.sortObjFunc(value)
        }
      }

      // 数组取value,否则取key+value(排除null,空,undefined)
      if (signUtil.isArraysFunc(plaintext)) {
        signStr += value
      } else {
        value !== null ? signStr += keyList[i] + value : signStr += keyList[i]
      }
    }
    return signStr
  },

  isArraysFunc (item) { return Object.prototype.toString.call(item) === '[object Array]' },

  isObjectFunc (item) { return Object.prototype.toString.call(item) === '[object Object]' },

  isHasValues (item) { return item !== 'null' || item !== 'undefined' || item !== 0 }
}


/**
 * aes加密
 * genKey: 获取key
 * encrypt: AES加密
 * decrypt: AES解密
 */
export const aesUtil = {
  genKey: function(expect = 16) {
    const random = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
    let str = ''
    while (str.length < expect) { str += random.charAt(Math.random() * random.length) }
    return str
  },

  encrypt: function(plaintext, key) {
    if (plaintext instanceof Object) { plaintext = JSON.stringify(plaintext) }
    let 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()
  },

  decrypt: function(ciphertext, key) {
    let 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) === '[') {
      decString = JSON.parse(decString)
    }
    return decString
  }
}


/**
 * rsa加密
 * encrypt: 公钥加密
 * decrypt: 私钥解密
 * ensign: rsa签名
 * design: rsa验签
 */
const encryptor = new JSEncrypt({ default_key_size: 1024 })
export const rsaUtil = {
  encrypt: function (key, publicKey) {
    publicKey && encryptor.setPublicKey(publicKey)
    return encryptor.encrypt(key)
  },

  decrypt: function (key, privateKey) {
    privateKey && encryptor.setPrivateKey(privateKey)
    return encryptor.decrypt(key)
  },

  ensign: function (data, privateKey){
    privateKey && encryptor.setPrivateKey(privateKey)
    return encryptor.sign(data, CryptoJS.SHA256, 'sha256')
  },

  design: function (data, signature, publicKey){
    if (signature && publicKey) {
      encryptor.setPrivateKey(publicKey)
      return encryptor.verify(data, signature, CryptoJS.SHA256)
    }
  }
}


/**
 * RSA秘钥对
 * publicKey: 前端rsa公钥
 * privateKey: 前端rsa私钥
 * servePublicKey: 服务端rsa公钥
 */
export const publicKey = encryptor.getPublicKey()
export const privateKey = encryptor.getPrivateKey()
export const servePublicKey = '服务端公钥'

第三步:改造Axios

import axios from 'axios'
import { signUtil, aesUtil, rsaUtil, servePublicKey, privateKey } from 'encryptn'

axios.defaults.timeout = 20000;
axios.defaults.baseURL = 'http://8.8.8.8:8080'
axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8';
axios.defaults.headers.post['Access-Control-Allow-Origin'] = '*';


// 请求拦截器
axios.interceptors.request.use((config) => {
  // other code here...  

  if (config.data) {
    // 生成签名
    const signKey = signUtil(config.data)
    const newSignKey = rsaUtil.ensign(signKey, privateKey)

    // AES随机秘钥
    const romkey = aesUtil.genKey()

    // 服务端公钥对随机秘加密
    const aesKey = rsaUtil.encrypt(romkey, servePublicKey)

    // AES + 随机秘对data体加密
    config.data['sign'] = newSignKey
    const aesData = aesUtil.encrypt(config.data, romkey)


    const newData = { data: aesData, key: aesKey }
    config.data = newData
  }

  return config;
}, (error) => {
  return Promise.reject(error);
});


// 响应拦截器
axios.interceptors.response.use((res) => {
  // other code here

  const { data, key } = res.data
  // 客户端私钥解密key
  const rsaKey = rsaUtil.decrypt(key, privateKey)

  // AES + key解密data
  const newData = aesUtil.decrypt(data, rsaKey)

  // 重新生成签名验证
  if (newData.sign) {
    const copyData = JSON.parse(JSON.stringify(newData))
    delete copyData.sign
    const data = signUtil(copyData)
    const flag = rsaUtil.design(data, newData.sign, servePublicKey)
    if (!flag) { return Promise.reject({ message: '签名失败' }) }
    return Promise.resolve(res.data)
  } else {
    return Promise.reject({ message: '请求异常' })
  }
}, (error) => {
    return Promise.reject(error);
});

4. 注意问题

签名排序统一,需要和后端签名一致才可以通过
加密长度统一,这里统一使用1024,具体前后端协商
数据结构统一,返回体的数据结构一致,方便后面验证签名
前端加密一定程度增加了网络攻击的难度,最好还是上https

文章目录