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

    技术2022-07-13  87

    目录

    0x00 四种API数据安全问题

    0x01 加密方式

    0x02 如何进行加密?

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

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

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

    0x06 授权sign唯一性支持

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


    0x00 四种API数据安全问题

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

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

    再通俗总结一下:

    1.接口不是你想调,想调就能调。 必须得有正确的sign才能调用接口

    2.sign不是你想有,想有就能有,必须要得知道双方约定的aeskey 和正确的加解密算法

    3.包不是你想抓,想抓就能抓。sign设置了过期时间,短到一旦抓包改包sign就会过期,就会失去调用接口的权限

    4.包中数据不是你想看,想看就能看。数据包中的关键数据都是经过加密的。

     

    0x01 加密方式

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

    0x02 如何进行加密?

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

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

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

    基础参数包括:

    sign 签名app_type  app类型(android、IOS、IOSPad等)app_version app版本号did 移动端设备的did (唯一的)可以理解为设备号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()); } }

     

    Processed: 0.016, SQL: 9