接口加密方案

* 本页面总结凯冰音乐APP项目接口加密方案的相关内容。

加密来啦!

接口加密研发同学来讲并不陌生,一些接口类型的项目里,尤其是这种需要对外网开放的接口,加密基本上应该算是“必备组件”,毕竟,安全性是非常之重要的!

而接口加密的思路一般也有以下几种,来保证API的安全:

  • √ 传输协议升级为HTTP/2(当然这个主要是提升性能的,详细总结可以移步 《程序员技能提升宝典》 手册的第一章第5节 HTTP/2 学习)
  • √ 接口数据传输采用HTTPS(详细总结可以移步 《程序员技能提升宝典》 手册的第一章第4节 HTTP/HTTPS 学习),可以保证接口在传输过程中即使被抓包工具抓到数据包,也看不到传输的原始信息
  • √ 前后台约定参数和数据加密方式,如经常使用的对称加密AES和非对称加密RSA,保证传输的信息在传输过程中是“密文”,无法被篡改,这是本节要详细总结的
  • × 增加额外验证信息,如token、md5等方式,最大程度保证数据未被篡改
  • √ 敏感信息进行脱敏处理,如证件号、手机号等(当然本项目里未涉及)

√ 表示我在“鹰眼”项目里采取的“保证安全”的手段,这节主要详细总结,项目是如何跟APP端配合,使用对称加密AES和非对称加密RSA结合的方式,对数据进行加密处理

先来简单分别认识下常用的这两种加密方案:RSA和AES!不过再介绍他们俩之前,还需要一个准备知识需要明确一下,那就是对称加密和非对称加密。

准备知识:对称加密和非对称加密
对称加密

对称加密采用了对称密码编码技术,它的特点是文件加密和解密使用相同的密钥加密。对称加密算法使用起来简单快捷,密钥较短,且破译困难,除了数据加密标准(DES),另一个对称密钥加密系统是国际数据加密算法(IDEA),它比DES的加密性好,而且对计算机功能要求也没有那么高。

常见的对称加密算法有DES、3DES、Blowfish、IDEA、RC4、RC5、RC6和AES

虽然对称加密使用方便,破译苦难,但是却有个非常“致命”的问题:一旦链接过多,那么就需要管理大量的秘钥key,这在一个复杂系统体系内,是个噩梦!非对称加密 就是在此环境下诞生的。

非对称加密

非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey),它们总是成对出现。如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密秘钥不同,所以叫非对称加密。

常见的非对称加密算法有RSA、ECC(移动设备用)、Diffie-Hellman、El Gamal、DSA(数字签名用)

在理解了对称加密和非对称加密的定义和区别之后,开始讲解我在项目中用到的两个加密方案!

对称加密方案:AES
对称加密方案:AES
对称加密方案:AES

高级加密标准(AES,Advanced Encryption Standard)为最常见的对称加密算法(微信小程序加密传输就是用这个加密算法的)。对称加密算法也就是加密和解密用相同的密钥,具体的加密流程如下图:

对称加密AES加密流程
对称加密AES加密流程

这里,我们暂时不研究密码学中是如何使用数学理论实现加密过程的,我们主要关注如何在程序中使用。对于Go项目,我们可以在GitHub上寻找封装好的加密组件包,我使用的是:"github.com/wumansgy/goEncrypt",下面是我的ASE实现代码:

  import (
    "crypto/aes"
    "crypto/cipher"
    "eagle/utils"
    "encoding/base64"
    "errors"
    "github.com/wumansgy/goEncrypt"
    "strconv"
  )

  const (
    aesTable = "这就是加密秘钥啦"
    AES_MODE_CBC = "CBC"
    AES_MODE_CTR = "CTR"
  )

  var (
    aesBlock       cipher.Block
    ErrAESTextSize = errors.New("ciphertext is not a multiple of the block size")
    ErrAESPadding  = errors.New("cipher padding size error")
  )


  func init() {
    var err error
    aesBlock, err = aes.NewCipher([]byte(aesTable))
    if err != nil {
      panic(err)
    }
  }

  func GetAesTable() []byte {
    return []byte(aesTable)
  }

  // AES解密
  func AesDecrypt(cryptText string, mode string, isString bool) (string, error) {
    var text []byte
    var err error

    //字符串转为字节数组
    cryptText2, _ := base64.StdEncoding.DecodeString(cryptText)

    switch mode {
    case AES_MODE_CBC:
      text ,err = goEncrypt.AesCbcDecrypt(cryptText2, []byte(aesTable))
    case AES_MODE_CTR:
      text ,err = goEncrypt.AesCtrDecrypt(cryptText2, []byte(aesTable))
    default:
      text ,err = goEncrypt.AesCbcDecrypt(cryptText2, []byte(aesTable))
    }

    if err != nil {
      return "", err
    }

    //是否是数字
    var result string
    if !isString {
      result = strconv.Itoa(utils.BytesToInt(text))
    }else{
      result = string(text)
    }

    return result, nil
  }


  // AES加密
  func AesEncrypt(cryptText []byte, mode string) (string, error) {
    var text []byte
    var err error

    switch mode {
    case AES_MODE_CBC:
      text ,err = goEncrypt.AesCbcEncrypt(cryptText, []byte(aesTable))
    case AES_MODE_CTR:
      text ,err = goEncrypt.AesCtrEncrypt(cryptText, []byte(aesTable))
    default:
      text ,err = goEncrypt.AesCbcEncrypt(cryptText, []byte(aesTable))
    }
    if err != nil {
      return "", err
    }

    return base64.StdEncoding.EncodeToString(text), nil
  }
非对称加密方案:RSA
非对称加密方案:RSA
非对称加密方案:RSA

RSA加密是一种非对称加密。可以在不直接传递密钥的情况下,完成解密。这能够确保信息的安全性,避免了直接传递密钥所造成的被破解的风险。是由一对密钥来进行加解密的过程,分别称为公钥和私钥。两者之间有数学相关,该加密算法的原理就是对一极大整数做因数分解的困难性来保证安全性。通常个人保存私钥,公钥是公开的(可能同时多人持有)。

需要注意的是,RSA加密对明文的长度有所限制,规定需加密的明文最大长度=密钥长度-11(单位是字节,即byte),所以在加密和解密的过程中需要分块进行。而密钥默认是1024位,即1024位/8位-11=128-11=117字节。所以默认加密前的明文最大长度117字节,解密密文最大长度为128字。那么为啥两者相差11字节呢?是因为RSA加密使用到了填充模式(padding),即内容不足117字节时会自动填满,用到填充模式自然会占用一定的字节,而且这部分字节也是参与加密的。秘钥长度可以进行自行调整,当然非对称加密随着密钥变长,安全性上升的同时性能也会有所下降。我在项目中使用的是2048位。

  package encrypt

  import (
    "encoding/hex"
    "github.com/wumansgy/goEncrypt"
    "io/ioutil"
  )

  const (
    msg = "sign模式签名"
    RSA_MODEL_NORMAL = "normal"
    RSA_MODEL_SIGN = "sign"
    PublicKeyPath = "/**/**/zkbhj_public.pem"
    PrivateKeyPath = "/**/**/zkbhj_private.pem"
  )

  // RSA解密
  func RsaDecrypt(cryptText string, mode string) (string, error) {
    var text []byte
    var err error
    var result bool
    var privateKey, publicKey []byte

    if privateKey, err = ioutil.ReadFile(PrivateKeyPath); err != nil {
      return "", err
    }
    if publicKey, err = ioutil.ReadFile(PublicKeyPath); err != nil {
      return "", err
    }

    //字符串转为字节数组
    cryptText2, _ := hex.DecodeString(cryptText)

    switch mode {
    case RSA_MODEL_NORMAL:
      text ,err = goEncrypt.RsaDecrypt(cryptText2, privateKey)
    case RSA_MODEL_SIGN:
      result = goEncrypt.RsaVerifySign([]byte(msg), cryptText2, publicKey)
    default:
      text ,err = goEncrypt.RsaDecrypt(cryptText2, privateKey)
    }

    //如果是数字签名类型且验证成功,返回nil
    if mode == RSA_MODEL_SIGN && result{
      return "", nil
    }

    if err != nil {
      return "", err
    }

    return string(text), nil
  }


  // RSA加密
  func RsaEncrypt(cryptText []byte, mode string) (string, error) {
    var text []byte
    var err error
    var privateKey, publicKey []byte

    if privateKey, err = ioutil.ReadFile(PrivateKeyPath); err != nil {
      return "", err
    }
    if publicKey, err = ioutil.ReadFile(PublicKeyPath); err != nil {
      return "", err
    }

    switch mode {
    case RSA_MODEL_NORMAL:
      text ,err = goEncrypt.RsaEncrypt(cryptText, publicKey)
    case RSA_MODEL_SIGN:
      text ,err = goEncrypt.RsaSign([]byte(msg), privateKey)
    default:
      text ,err = goEncrypt.RsaEncrypt(cryptText, publicKey)
    }
    if err != nil {
      return "", err
    }

    return hex.EncodeToString(text), nil
  }
如何进行前后台的配合呢?

后台实现了加解密之后,那么如何跟前台APP进行配合呢?主要是下面的流程步骤:

  • 服务端生成一对RSA秘钥,私钥放在服务端(不可泄露),公钥下发给客户端,客户端在每次启动时,通过接口获取最新的公钥(服务器每次启动会重新生成),通过对比MD5值决定是否更新本地公钥
  • 对需要AES加密的接口,客户端调用接口API拿到AES秘钥(秘钥经过RSA加密算法加密,客端解开后可拿到真正的AES秘钥)
  • APP使用拿到的AES秘钥对数据进行加密,后台接口对AES数据进行解密
  • APP通过RSA公钥加密的内容,后台直接通过私钥即可解密
加密示例
加密示例

如上图所示,经过加密之后,原文“cRqNIeQ6mTzm96s2O4PSpA==”(本身是个AES加密结果),经过RSA加密之后密文是:

8b44a9d95e33bc66d0fa406ea5fbdf960c3c7a28cbb1db2fa795db3581124e8e7cb6a58137c92f0d5261914b9b98e6c0bc95e771c2b49cbaf442e1b3bc6cb8fd6ea50a0a8835da163f9975154d9e78a1834623de3161d6b55043234401cad01c09da4680ad07d7862d5bbcf18a3e13b2d4803f2e550e82972382c9f1528effd2de1f9b00895387f5a56bc4dd743acb4f3319749bcd40fd2096cb2e3a354910ada069dd07de2d367e7ae4506ecb9fa7646456c8ed0ca8ed7854c033a77fa6eae93516515a50b3ee6d8280f6bfa205d9721d0dc059140aaf0b6c45091a313b1ed1473d6126a02d0b29cba19290d0f8017f1a178429f34220f36a6ac1dc2082aa9b

这样,在读取歌曲音频地址的时候,就可以起到加密的作用,后台拿到这个数据之后,可以通过私钥解密,得到AES的加密结果,然后再通过AES秘钥解密,拿到真正的数据,处理之后,返回真正的音频地址,前端进行播放。比如上面的加密结果,就会得到请求后台的歌曲地址,如下:

https://mp3.zkbhj.com/app/music/media/yc/8b44a9d95e33bc66d0fa406ea5fbdf960c3c7a28cbb1db2fa795db3581124e8e7cb6a58137c92f0d5261914b9b98e6c0bc95e771c2b49cbaf442e1b3bc6cb8fd6ea50a0a8835da163f9975154d9e78a1834623de3161d6b55043234401cad01c09da4680ad07d7862d5bbcf18a3e13b2d4803f2e550e82972382c9f1528effd2de1f9b00895387f5a56bc4dd743acb4f3319749bcd40fd2096cb2e3a354910ada069dd07de2d367e7ae4506ecb9fa7646456c8ed0ca8ed7854c033a77fa6eae93516515a50b3ee6d8280f6bfa205d9721d0dc059140aaf0b6c45091a313b1ed1473d6126a02d0b29cba19290d0f8017f1a178429f34220f36a6ac1dc2082aa9b/29435453435124.mp3

这样,就完成了一次前后端的加密数据交互。

前端如何完成加密?

这里只介绍RSA的加密方案,AES类似。因为是Dart语言的,所以如果没有接触过的可能对这个写法会有些奇怪,不过理解思路即可,实际使用时可以使用对应编程语言的版本,如Java或者Swift。

  //程序启动更新RSA公钥
  static Future fetchRsaPublicSecret() async {
    var nowTime = new DateTime.now().millisecondsSinceEpoch;
    var reqData = {
      'time_stamp': nowTime
    };
    var response = await http.post('/app/exp-url/c-act.json', data: reqData);
    var rsaPublicSecret = new RsaPublicSecret.fromJsonMap(response.data);

    try {
      // 获取应用文档目录并创建文件
      final dir = await getApplicationDocumentsDirectory();
      final file = File('${dir.path}/rsa/pub.pem');

      if (await file.exists()) {
        //判断是否需要更新本地秘钥文件
        var publicKeyStr = await file.readAsString();
        var fileVersion = CommonTools.generateMd5(publicKeyStr);
        if (fileVersion == rsaPublicSecret.version) {
          return;
        }
      }

      await file.writeAsString(rsaPublicSecret.secret);
      await file.exists();

    } catch (e) {
      print('RSA公钥下载有异常!$e');
      return null;
    }

    return;
  }
  Future getSongUrl(Song s) async {
    //调用接口返回歌曲链接
    var nowTime = new DateTime.now().millisecondsSinceEpoch;
    //RSA计算加密结果
    var hash = await Rsa.encodeString(s.songHash);
    var mode = _songData.playMode;
    return 'Baseurl/$mode/$hash/$hash2.mp3';
  }
  import 'dart:io';
  import 'package:path_provider/path_provider.dart';
  import 'package:flutter/foundation.dart';
  import 'package:encrypt/encrypt.dart';

  class Rsa {

    /// RSA加密
    static Future encodeString(String content) async{
      final dir = await getApplicationDocumentsDirectory();
      final file = File('${dir.path}/rsa/pub.pem');
      if (await file.exists() == false) {
        return "";
      }
      await file.exists();

      var publicKeyStr = await file.readAsString();
      var publicKey = RSAKeyParser().parse(publicKeyStr);
      final encrypter = Encrypter(RSA(publicKey: publicKey));
      var result = encrypter.encrypt(content).base16.toUpperCase();

      return result;
    }

  }

以上就是整个加密方案的整体记录和总结,希望可以给有同样使用场景的大家带来帮助。


* 本页内容参考以下数据源:

  • https://www.cnblogs.com/lakei/p/11165987.html
  • https://www.liuvv.com/p/8ba026f1.html
  • https://blog.csdn.net/qq_28205153/article/details/55798628
  • https://www.cnblogs.com/pcheng/p/9629621.html

凯冰科技 · 代码改变世界,技术改变生活
下一篇:对GET传参长度限制的误解 →