ThinkPHP5开发中API数据安全相关解决方案

目录

0x00 四种API数据安全问题

0x01 加密方式

0x02 如何进行加密?

0x03 将基础参数(app版本号,手机型号,sign等)放入http协议的header头

0x04 AES加密解密算法的使用,sign算法生成

0x05 优化:设置sign失效时间

0x06 授权sign唯一性支持

0x07 APP和服务器端时间一致性解决方案


0x00 四种API数据安全问题

  1. 接口请求地址 和 参数暴露
  2. 重要接口返回数据明文暴露
  3. APP登录态请求的数据安全性问题
  4. 代码层的数据安全问题

其中前三种问题,都可以使用加密来解决。

0x01 加密方式

  1. MD5
  2. AES 对称加密算法,加密速度快,资源使用率高
  3. RSA 非对称加密算法,加密效率低,数据量大的时候加密时间比较长

0x02 如何进行加密?

  1. 将基础参数(app版本号,手机型号,sign等)放入http协议的header头中
  2. 每次http请求都携带sign
  3. 保证sign的唯一性
  4. 请求参数、返回数据按安全性适当加密
  5. access_token

0x03 将基础参数(app版本号,手机型号,sign等)放入http协议的header头

一般情况下,会将业务相关的数据放在HTTP协议的Body中,而将其他基础参数放在header头中

基础参数包括:

  1. sign 签名
  2. app_type  app类型(android、IOS、IOSPad等)
  3. app_version app版本号
  4. did 移动端设备的did (唯一的)可以理解为设备号
  5. model app的机型

ThinkPHP5 相关函数:

request()->header() 获取HTTP报文的header头中的数据

0x04 AES加密解密算法的使用,sign算法生成

思路:

客户端将header头中的version did等字段加密成 sign 和version did 一起发给服务端,服务端将sign的解密,然后将解密后的值

和version did等比对。如果比对合同,那么就通过效验。

sign 加密需要客户端工程师去做,解密则需要服务端工程师去做

相关函数:

http_build_query($arr) 将数组转化为&连接的字符串

parse_str($str,$arr) 将&连接的str转化为数组存在$arr中

代码:

common下的公共加解密类库

<?php
 
namespace app\common\lib;
/**
 * aes 加密 解密类库
 *
 */
class Aes {
 
    private $hex_iv = '00000000000000000000000000000000'; # converted JAVA byte code in to HEX and placed it here
 
    private $key = null;
 
    function __construct() {
        $this->key = config('app.aeskey');
        $this->key = hash('sha256', $this->key, true);
    }
 
   
 
    public function encrypt($input)
    {
        $data = openssl_encrypt($input, 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA, $this->hexToStr($this->hex_iv));
        $data = base64_encode($data);
        return $data;
    }
 
    public function decrypt($input)
    {
        $decrypted = openssl_decrypt(base64_decode($input), 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA, $this->hexToStr($this->hex_iv));
        return $decrypted;
    }
 
    /*
      For PKCS7 padding
     */
 
    private function addpadding($string, $blocksize = 16) {
 
        $len = strlen($string);
 
        $pad = $blocksize - ($len % $blocksize);
 
        $string .= str_repeat(chr($pad), $pad);
 
        return $string;
 
    }
 
    private function strippadding($string) {
 
        $slast = ord(substr($string, -1));
 
        $slastc = chr($slast);
 
        $pcheck = substr($string, -$slast);
 
        if (preg_match("/$slastc{" . $slast . "}/", $string)) {
 
            $string = substr($string, 0, strlen($string) - $slast);
 
            return $string;
 
        } else {
 
            return false;
 
        }
 
    }
 
    function hexToStr($hex)
    {
 
        $string='';
 
        for ($i=0; $i < strlen($hex)-1; $i+=2)
 
        {
 
            $string .= chr(hexdec($hex[$i].$hex[$i+1]));
 
        }
 
        return $string;
    }
 
}

IAuth.php

<?php
/*
 * @Author: your name
 * @Date: 2020-06-26 22:30:46
 * @LastEditTime: 2020-07-02 16:40:23
 * @LastEditors: Please set LastEditors
 * @Description: In User Settings Edit
 * @FilePath: /myNewsApp/application/common/lib/IAuth.php
 */ 
namespace app\common\lib;
use app\common\lib\aes;

class IAuth{
    /**
     * 明文密码加盐加密
     * @param string $data 明文密码
     * @return string
     */
    public static function setPassword($data){
        return md5($data.config('app.password_salt'));
    }

    /**
     * 检查sign的合法性
     * @param string $sign
     * @param array $data  header头中相关数据组成的数组
     * @return bool
     */
    public static function checkSignPass($sign='',$data){
        $str = (new Aes())->decrypt($sign);
        if(empty($str)){
            return false;
        }
        parse_str($str,$arr);
        if(!is_array($arr)
         || empty($arr['did'] 
         || $arr['did']!=$data['did'])
         || $arr['version']!=$data['version']){
            return false;
        }
        return true;

    }
    /**
     * 生成每次请求的sign
     * @param array $data
     * @return string
     */
    public static function setSign($data=[]){
        //1.按key排序
        ksort($data);
        //2.数组转url传参的格式 id=1&username=123这种格式
        $string = http_build_query($data);
        //3.通过aes来加密
        $string = (new Aes())->encrypt($string);
        return $string;
    }
}

Common控制器

<?php
/*
 * @Author: Shang Rui
 * @Date: 2020-07-02 12:48:34
 * @LastEditTime: 2020-07-02 16:43:00
 * @LastEditors: Please set LastEditors
 * @Description: In User Settings Edit
 * @FilePath: /myNewsApp/application/api/controller/Common.php
 */ 
namespace app\api\controller;
use think\Controller;
use app\common\lib\Aes;
use app\common\lib\IAuth;
use app\common\lib\exception\ApiException;

/**
 * API模块公共控制器
 */
class Common extends Controller{
    public $headers = [];
   /**
    * 初始化方法
    */
   public function _initialize(){
        // $this->testAes();
       $this->checkRequestAuth();

   }
   /**
    * 检查每次app请求的数据是否合法
    */
   public function checkRequestAuth(){

       //首先需要获取header头
       $headers = request()->header();
       //todo
       //sign加密
       //基础参数校验
       if(empty($headers['sign'])){
            throw new ApiException('sign不存在',400);
       }
       if(!in_array($headers['app_type'],config('app.apptypes'))){
           throw new ApiException('app_type不合法',400);
       }
       //校验sign的合法性
       if(!IAuth::checkSignPass($headers['sign'],$headers)){
           throw new ApiException('授权码sign失败!',401);
       }
       $this->headers = $headers;

   } 
   
   public function testAes(){
       $data = [
           'did'=>'123',
           'version'=>1,
       ];
       $decode = IAuth::setSign($data);
       echo $decode;
       echo (new Aes())->decrypt($decode);
       exit;
   }
}

0x05 优化:设置sign失效时间

思路:如果sign解密 和 加密之间的时间差 超过了 该时间 ,我们就认为 sign无效。例如:黑客对前端向后端发送的数据进行了抓包,但是如果黑客抓包改包发送请求 这一系列操作 超过了我们配置的失效时间,我们后端就认为sign无效了。

代码:

加密:

       $data = [
           'did'=>'123',
           'version'=>1,
           'time'=>Time::get13TimeStamp(),
       ];
       $decode = IAuth::setSign($data);

解密:

    /**
     * 检查sign的合法性
     * @param string $sign
     * @param array $data  header头中相关数据组成的数组
     * @return bool
     */
    public static function checkSignPass($sign='',$data){
        $str = (new Aes())->decrypt($sign);
        if(empty($str)){
            return false;
        }

        parse_str($str,$arr);
        if(time()-ceil($arr['time']/1000) > config('app.app_sign_time')){
            return false;
        }
        if(!is_array($arr)
         || empty($arr['did'] 
         || $arr['did']!=$data['did'])
         || $arr['version']!=$data['version']){
            return false;
        }
        return true;

    }

生成13位时间戳:

一般情况下时间戳都是10位,我们可以使用microtime:返回字符串 "microsec sec" ,其中 sec 为自 Unix 纪元(0:00:00 January 1, 1970 GMT)起的秒数,microsec 为微秒部分。

在微妙数部分取三位 和 十位秒数部分拼接,得到13位的时间戳

为什么要生成13位的时间戳,因为更多位数的时间戳 被加密后唯一性更强一点

代码:

<?php
/*
 * @Author: your name
 * @Date: 2020-07-07 10:15:34
 * @LastEditTime: 2020-07-07 10:28:27
 * @LastEditors: Please set LastEditors
 * @Description: In User Settings Edit
 * @FilePath: /myNewsApp/application/common/lib/Time.php
 */ 
namespace app\common\lib;

class Time{
    /**
     * @description: 获取13位的时间戳
     * @param {type} 
     * @return: int
     */    
    public static function  get13TimeStamp(){
        list($t1,$t2)=explode(' ',microtime());
        return $t2.ceil($t1*1000);
    }
}

0x06 授权sign唯一性支持

思路:每个sign只能使用一次。

当一个sign被效验成功后,我们就在 文件/MySQL/Redis中 写入一个标记,标记该sign已经被效验。

当下次一个sign被发过来,我们读取标记,判断sign是否已经被使用过了。

如果你的代码都保存在同一台服务器上 ,可以写入文件中,建议使用TP5的Cache机制写入文件

如果是分布式架构,建议写入到MySQL或者Redis中

补充:

Cache::get(键名,键值,过期时间) 写入缓存文件(位置:cache文件夹下)

Cache::set(键名) 读取缓存文件中对应的内容。

cache() 封装的助手函数

一般情况下我们的缓存文件不能一直保存,这样会占用服务器的资源。所以必须有一个过期时间。但是这样不就只能保证:每个sign在过期时间内只能使用一次吗?只要将缓存过期时间 > sign的失效时间,就可以实现每个sign只能使用一次的效果了。

代码:

       //校验sign的合法性
       if(!IAuth::checkSignPass($headers['sign'],$headers)){
           throw new ApiException('授权码sign失败!',401);
       }
       Cache::set($headers['sign'],1,config('app.app_sign_cache_time'));
        //检查sign的唯一性
        if(Cache::get($sign)){
            return false;
        }

0x07 APP和服务器端时间一致性解决方案

问题背景:app端 和服务器端 的时间 并不一定完全一致,可以存在一个时间差,这为之前的sign失效时间的验证造成了问题。

解决思路:

服务器端开一个接口,让app端可以获取到服务器端的时间

<?php
/*
 * @Author: your name
 * @Date: 2020-07-07 11:39:29
 * @LastEditTime: 2020-07-07 11:45:24
 * @LastEditors: Please set LastEditors
 * @Description: In User Settings Edit
 * @FilePath: /myNewsApp/application/api/controller/Time.php
 */ 
namespace app\api\controller;
use think\Controller;

class Time extends Controller{
    public function index(){
        return show(config('api.success_code'),'获取时间成功!',time());
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章