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

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

背景介绍

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

  • 加密方法:常用的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的,所以要单独设置一下。

总结

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

  • 刚开始本来是准备一个月最少写一篇的,但是由于八月份刚换工作,不太有时间去写,所以也是一直拖着;而且是用公司电脑写的这篇文章,所以代码都没有直接粘贴过来,可能会存在疏漏,请多多包涵哈;
相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax