目录
0x03 将基础参数(app版本号,手机型号,sign等)放入http协议的header头
0x00 四种API数据安全问题
- 接口请求地址 和 参数暴露
- 重要接口返回数据明文暴露
- APP登录态请求的数据安全性问题
- 代码层的数据安全问题
其中前三种问题,都可以使用加密来解决。
0x01 加密方式
- MD5
- AES 对称加密算法,加密速度快,资源使用率高
- 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());
}
}