记录一次接口加密的实现方案

隔了三个月才写了这篇文章,实在是莫得时间去写,踩坑很多但是输出很少,后面有时间也会多记录一些自己的踩坑经历,要是能给各位同学有所帮助那是最好的了,废话不多说,进入正题了。

背景介绍

由于部门业务体量正在提升,为了防止数据盗取或者外部攻击,对接口进行加密提上了日程,部门的大佬们也讨论了各种加密方式,考虑各种情况,最终敲定了方案。说到我们常用的数据加密方法,方式是各种各样的,根据我们实际的业务需求,我们可以选择其中的一种或者几种方式方法进行数据加密处理。

  • 加密方法:常用的AES,RSA,MD5,BASE64,SSL等等;
  • 加密方式:单向加密,对称加密,非对称加密,加密盐,数字签名等等;

首先我们来简单分析一下上面说到的这几种加密有什么区别吧:

  • AES加密:对称加密的方法,加解密使用相同的加密规则,密钥最小能够支持128,192,256位(一个字节8位,后面我使用的是16位字符);
  • RSA加密:非对称加密的方法,加解密使用一对公钥私钥进行匹配,客户端使用公钥加密,服务端使用私钥进行解密;
  • MD5加密:单向加密,加密后不可解密,只能通过相同的数据进行相同的加密再与库中数据进行对比;
  • BASE64:一种数据编码方式,伪加密,把数据转化为BASE64的编码形式,通过A-Z,a-z,0-9,+,/ 共64个字符对明文数据进行转化;
  • SSL加密:https协议使用的加密方式,使用多种加密方式进行加密(具体使用哪些,我也不了解,感兴趣的同学可以去搜一下告诉我哈);

想要详细了解各类加密方式方法的同学,可以自行百度一下哈,这里就不进行赘述了,之后就来详细讲一下本次使用的加密方式。本次为了更加全面加密,使用了AES,RSA,以及加密盐,时间戳,BASE64与BASE16转化等方式进行加密处理。

请求体AES加密

请求体使用AES的对称加密方式,每次接口请求会随机生成一个16位的秘钥,使用秘钥对数据进行加密处理,返回的数据也会使用此秘钥进行解密处理。

js 复制代码
import CryptoJs from 'crypto-js'// AES加密库
import { weAtob } from './weapp-jwt' // atob方法

// 请求体加密方法
export const encryptBodyEvent = (data, aeskey, isEncryption) => {
    // 请求体内容
    const wirteData = {
        data: data, // 接口数据
        token: Taro.getStorageSync("authToken"), // token 校验
        nonce: randomNumberEvent(32), // 32位随机数,接口唯一随机数,可查询服务日志
        timestamp: new Date().getTime, // 时间戳,用于设置接口调用过期时间
    }
    const encryptBodyJson = CryptoJs.AES.encrypt(JSON.stringify(wirteData), CryptoJs.enc.Utf8.parse(aeskey), {
        mode: CryptoJs.mode.ECB,
        padding: CryptoJs.pad.Pkcs7
    }).toString()
    // 判断接口是否需要加密
    // 服务接收BASE16数据,Base64toHex方法为BASE64转化为BASE16方法
    return isEncryption ? Base64toHex(encryptBodyJson) : wirteData
}

// BASE64转化BASE16方法
function Base64toHex (base64) {
    let raw = weAtob(base64)
    let HEX = ""
    for (let i=0; i < raw.length; i++) {
        let _HEX = raw.charCodeAt(i).toString(16)
        HEX = (_HEX.length == 2 ? _HEX : "0" + _HEX)
    }
    return HEX
}

// 生成n位随机数,默认生成16位
function randomNumberEvent (length = 16) {
    let str = ""
    let arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
    // 随机产生
    for(let i=0; i < length; i++){
        let pos = Math.round(Math.random() * (arr.length-1));
        str += arr[pos];
    }
    return str
}
  • mode是分组加密模式:有五种模式(ECB、CBC、CFB、OFB、CTR),这里我们使用最简单的ECB模式,明文分组加密之后的结果将直接成为密文分组,对其他几种模式感兴趣的可以去搜索一下几种模式的区别;
  • padding是填充模式:正常的加密后的字节长度不可能刚刚好满足固定字节的对齐(块大小),所以需要进行一定的填充,常用的有三种模式(PKCS7、PKCS5、Zero,还有其他模式),这里我们使用的是PKCS7模式。假设数据长度需要填充n个字节才对齐,那么填充n个字节,每个字节都是n;假设数据本身就已经对齐了,则填充一个长度为块大小的数据,每个字节都是块大小;
  • weAtob即小程序使用的atob方法:atob是JS的一个全局函数,用于将BASE64编码转化为原始字符串,在正常的H5项目中atob可以直接使用,但是在小程序中此方法不可用,因此使用一个手动实现的方式(文件就不上传了,电脑是加密的,上传也是乱码,网上也是能找到类似的方法);
  • timestamp是用于防止过期调用:这里的时间是为了展示方便直接使用客户端时间,实际是会调用一个服务端的接口获取服务器时间进行时间校准,防止客户端手动修改时间,服务端设置过期时间,会根据传入的时间判断是否过期;

请求头RSA加密

看完上面的请求体加密,我们会想到一个问题,就是我们的aesKey是客户端随机生成的,但是服务端也需要这个aesKey进行数据的加解密,那么我们通过什么形式传给服务端呢?因此我们在请求头中设置一个secret-key字段,使用RSA中的公钥对aesKey进行加密,服务端使用对应私钥进行解密;

js 复制代码
// import JSEncrypt from 'jsencrypt' // RSA加密库,小程序不支持
import WxmpRsa from 'wxmp-rsa' // RSA加密库,小程序支持

let public_key = 'xxxxxxxxxxxxxxxx' // 公钥
// 请求头加密方法
export const randomKeyEvent = (aesKey) => {
    // JSEncrypt方法小程序不可用
    // const RSAUtils = new JSEncrypt() // 新建JSEncrypt对象
    // RSAUtils.setPublicKey(public_key) // 设置公钥
    // return RSAUtils.encrypt(aesKey).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')

    const RSAUtils = new WxmpRsa() // 新建WxmpRsa对象
    RSAUtils.setPublicKey(public_key) // 设置公钥
    // 进行RSA加密后,生成字符串中的部分特殊字符在服务端会被自动转化为空格,导致解密失败,所以先进行转换处理
    return RSAUtils.encryptLong(aesKey).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')
}
  • JSEncrypt在小程序不可用是由于库里面存在window对象以及navigator对象,但是小程序没有对应的方法,所以使用了一个优化后的wxmp-rsa库;
  • replaceAll处理字符是因为RSA加密后,生成字符串中的部分特殊字符传给服务端会被自动转化为空格,导致解密失败,所以需要进行转换处理,为了兼容低版本replaceAll方法不支持可使用replace加正则进行替换;

返回体AES解密

服务端返回的数据内容使用了相同的AES加密方法,因此也需要使用AES进行数据解密处理,并且返回的数据是BASE16,因此还需要进行一次编码转换处理;

js 复制代码
// 返回体解密方法
export const decryptBodyEvent = (data, aeskey) => {
    // HexToBase64为BASE16转化为BASE64方法
    const responseData = CryptoJs.AES.decrypt(HexToBase64(data), CryptoJs.enc.Utf8.parse(aeskey), {
        mode: CryptoJs.mode.ECB,
        padding: CryptoJs.pad.Pkcs7
    }).toString(CryptoJs.enc.Uth8)
    return JSON.parse(responseData)
}

// base16转base64 网上找个一个方法,应该有其他简单的实现方式
function HexToBase64 (sha1) {
  var digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
  var base64_rep = ""
  var ascv
  var bit_arr = 0
  var bit_num = 0

  for (var n = 0; n < sha1.length; ++n) {
    if (sha1[n] >= 'A' && sha1[n] <= 'Z') {
      ascv = sha1.charCodeAt(n) - 55
    } else if (sha1[n] >= 'a' && sha1[n] <= 'z') {
      ascv = sha1.charCodeAt(n) - 87
    } else {
      ascv = sha1.charCodeAt(n) - 48
    }

    bit_arr = (bit_arr << 4) | ascv
    bit_num += 4
    if (bit_num >= 6) {
      bit_num -= 6
      base64_rep += digits[bit_arr >>> bit_num]
      bit_arr &= ~ (-1 << bit_num)
    }
  }

  if (bit_num > 0) {
    bit_arr <<= 6 - bit_num
    base64_rep += digits[bit_arr]
  }
  var padding = base64_rep.length % 4
  if (padding > 0) {
    for (var n = 0; n < 4 - padding; ++n) {
      base64_rep += "="
    }
  }
  return base64_rep
}

封装接口

因为是小程序项目,使用的是Taro框架进行封装的,Vue中使用axios封装其实也是类似的,还封装了一套ajax的方法,除了接口这里封装有区别,加密都是类似的。

js 复制代码
const baseUrl = 'https://xxx.xxx.com' // 接口请求头

// Taro封装接口方法
export const requestEncHttp ({url, data, isEncryption = true}) => {
    // 每次调用都会随机生成一个动态的aesKey,防止接口被复用
    const aesKey = randomNumberEvent()
    return new Promise((resolve, reject) => {
        Taro.request({
            method: "POST",
            header: {
                "content-type": "application/json",
                "secret-key": isEncryption ? randomKeyEvent(aesKey): ''
            },
            dataType: 'text',
            data: encryptBodyEvent(data, aesKey, isEncryption),
            url: baseUrl + url,
            success: (result) => {
                if(result.status === 200) {
                    resolve(isEncryption ? decryptBodyEvent(result.data, aesKey) : JSON.parse(result.data))
                } else {
                    reject(result)
                }
            }, fail: (err) => {
                reject(err)
            }
        })
    })
}
  • dataType使用text:ajax没有此问题,Taro框架会出现接口有返回数据,但是在success中接收不到数据,因为数据是BASE16形式,Taro封装的数据返回格式默认应该是JSON的,所以要单独设置一下。

总结

加密的方式有很多,这篇文章也只是浅尝即止,想更加详细了解的同学可以再搜一些大佬们的总结文章,我这里也只是结合业务做了一点总结,标注一下踩坑点;

  • 刚开始本来是准备一个月最少写一篇的,但是由于八月份刚换工作,不太有时间去写,所以也是一直拖着;而且是用公司电脑写的这篇文章,所以代码都没有直接粘贴过来,可能会存在疏漏,请多多包涵哈;
相关推荐
王哈哈^_^42 分钟前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie1 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic2 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿2 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具2 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
清灵xmf3 小时前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
小白学大数据3 小时前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_390161773 小时前
防抖函数--应用场景及示例
前端·javascript
334554324 小时前
element动态表头合并表格
开发语言·javascript·ecmascript
John.liu_Test4 小时前
js下载excel示例demo
前端·javascript·excel