接口的安全性主要围绕Token、Timestamp和Sign三个机制展开设计,保证接口的数据不会被篡改和重复调用,下面具体来看:
(1)Token授权机制:(Token是客户端访问服务端的凭证)--用户使用用户名密码登录后服务器给客户端返回一个Token(通常是UUID),并将Token-UserId以键值对的形式存放在缓存服务器中。服务端接收到请求后进行Token验证,如果Token不存在,说明请求无效。
(2)时间戳超时机制:(签名机制保证了数据不会被篡改)用户每次请求都带上当前时间的时间戳timestamp,服务端接收到timestamp后跟当前时间进行比对,如果时间差大于一定时间(比如5分钟),则认为该请求失效。时间戳超时机制是防御DOS攻击的有效手段。
(3)签名机制:将 Token 和 时间戳 加上其他请求参数再用MD5或SHA-1算法(可根据情况加点盐)加密,加密后的数据就是本次请求的签名sign,服务端接收到请求后以同样的算法得到签名,并跟当前的签名进行比对,如果不一样,说明参数被更改过,直接返回错误标识。
/** * @desc 接受参数处理 */ private function dealParam(){ //接受header参数--系统参数 $systemParam=getAllHeadersParam(); //接受body数据--业务参数(json格式) $data=file_get_contents(\'php://input\'); //读取配置文件中的私钥信息 $api_apiKey=C(\'api_apiKey\'); $privatekey=$api_apiKey[$systemParam[\'token\']]; $arr[\'token\'] =$systemParam[\'token\']; //服务端分配的标识(不同客户端需使用不同的标识) $arr[\'timestamp\']=$systemParam[\'timestamp\']; //时间戳,UTC时间,以北京时间东八区(+8)为准 $arr[\'version\'] =$systemParam[\'version\']; //版本号 $arr[\'sign\'] =$systemParam[\'sign\']; //签名 $arr[\'source\'] =$systemParam[\'source\']; //来源(0-安卓/1-IOS/2-H5/3-PC/4-php/5-java) $arr[\'data\'] =json_decode($data,true); //业务参数json格式 $arr[\'method\'] =$data[\'method\']; //访问接口,格式:模型名.方法名 return $arr; }
/* * @desc 获取所有以HTTP开头的header参数 * @return array */ private function getAllHeadersParam(){ $headers = array(); foreach($_SERVER as $key=>$value){ if(substr($key, 0, 5)===\'HTTP_\'){ $key = substr($key, 5); $key = str_replace(\'_\', \' \', $key); $key = str_replace(\' \', \'-\', $key); $key = strtolower($key); $headers[$key] = $value; } } return $headers; }
/* * @desc 签名校验 * @param $token string 服务端分配的标识(不同客户端需使用不同的标识) * @param $timestamp string 时间戳,UTC时间,以北京时间东八区(+8)为准 * @param $version string 版本号 * @param $sign string 签名 * @param $source int 来源(0-安卓/1-IOS/2-H5/3-PC/4-php/5-java) * @param $privatekey string 私钥 * @param $data 业务参数json格式 * @return bool */ private function checkAuth($token,$timestamp,$version,$sign,$source,$privatekey,$data){ //参数判断 if(empty($token)){ E(\'token不能为空!\'); } if(empty($timestamp)){ E(\'时间戳不能为空!\'); } if(empty($version)){ E(\'版本号不能为空!\'); } if(empty($data)){ E(\'业务参数不能为空!\'); } if(empty($source) && $source<>\'0\'){ E(\'来源不能为空!\'); } if(empty($sign)){ E(\'签名不能为空!\'); } if(empty($privatekey)){ E(\'私钥不能为空!\'); } //时间校验 $expire_second=C(\'expire_second\',null,10); $timestamp_t=$timestamp+$expire_second; if($timestamp_t<time()){ E(\'请求已经过期!\'); } $public= D(\'public\'); $datas=$this->original; //系统参数 $paramArr=array( \'token\'=>$token, \'timestamp\'=>$timestamp, \'version\'=>$version, \'source\'=>$source, \'data\'=>$data, ); //按规则拼接为字符串 $str = $this->createSign($paramArr,$this->privatekey); if($str != $this->sign){ E(\'验签错误!\'); } return true; }
sign生成规则及步骤:
① 第一步:将所有需要发送至服务端的请求参数(空参数值的参数、文件、字节流、sign除外)按照参数名ASCII码从小到大排序(字典序)
注意:
l 参数名ASCII码从小到大排序(字典序);
l 如果参数的值为空不参与签名;
l 文件、字节流不参与签名;
l sign不参与签名;
l 参数名、参数值区分大小写;
② 第二步:将排序后的参数按照URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串strA;
③ 第三步:在strA后面拼接上apiKey得到striSignTemp字符串,将strSignTemp字符串转换为小写字符串后进行MD5运算,MD5运算后得到值作为sign的值传入服务端;
示例(所有参数、参数值均为示例,开发人员参考格式即可):
token:cd171009328172Ad3sc
apiKey:cd13H2ddd22212ds1da
① 第一步(获取到的请求参数并按照参数名ASCII码从小到大排序):
token=cd173309328172Ad322
data={"userName":"18817201899",goods:["addrId":323,{"skuNo":"p12232-023","count":3},{"skuNo":"p12232-013","count":1}]}
timestamp=1507537036
version=v3.6.0
② 第二步(按规则拼接为字符串strA):
token=cd171009328172Ad3sc&data={"userName":"18817201899",goods:["addrId":323,{"skuNo":"p12232-023","count":3},{"skuNo":"p12232-013","count":1}]}timestamp=1507537036&version=v3.6.0
③ 第三步(生成sign):
1)待签名字符串strSignTemp:
token=cd171009328172Ad3sc&data={"userName":"18817201899",goods:["addrId":323,{"skuNo":"p12232-023","count":3},{"skuNo":"p12232-013","count":1}]}timestamp=1507537036&version=v3.6.0cd13H2ddd22212ds1da
2)转换为小写字符串
strtolower()
3)MD5加密后的密文
6D556D52822658FD47F7FE362544CEE1
/* * @desc 签名函数 * @param $paramArr 系统参数 * @param $apiKey 私钥 * @return string 返回签名 */ private function createSign ($paramArr,$apiKey) { ksort($paramArr); $sign=\'\'; foreach ($paramArr as $key => $val) { if ($key != \'\' && $val != \'\') { $sign .= $key."=".$val."&"; } } $sign=rtrim($sign,"&"); $sign.=$apiKey; $sign=strtolower($sign); $sign = md5($sign); return $sign; }
(4)拒绝重复调用:客户端第一次访问时,将签名sign存放到缓存服务器中,超时时间设定为跟时间戳的超时时间一致,二者时间一致可以保证无论在timestamp限定时间内还是外 URL都只能访问一次。如果有人使用同一个URL再次访问,如果发现缓存服务器中已经存在了本次签名,则拒绝服务。如果在缓存中的签名失效的情况下,有人使用同一个URL再次访问,则会被时间戳超时机制拦截。这就是为什么要求时间戳的超时时间要设定为跟时间戳的超时时间一致。拒绝重复调用机制确保URL被别人截获了也无法使用(如抓取数据)。
/** * @desc 限制请求接口次数 * @return bool */ private function ask_count(){ $client_ip = $this->sys_get_client_ip(); $ask_url = $this->sys_GetCurUrl(); //限制次数 $limit_num = C(\'api_ask_limit\',null,5); //有效时间内,单位:秒 $limit_time = C(\'api_ask_time\'); $now_time = time(); $valid_time = $now_time - $limit_time; $ipwhere[\'creatime\'] = array(\'EGT\',date(\'Y-m-d H:i:s\',$valid_time)); $ipwhere[\'ip_name\'] = $client_ip; $ipwhere[\'ask_url\'] = $ask_url; $check_result = M(\'log_ip_ask\')->where($ipwhere)->count(); if($check_result !==\'0\'){ if($check_result >= $limit_num){ E(\'已经超出了限制次数!\'); } } //执行插入 $add_data = array( \'ip_name\'=>$client_ip, \'ask_url\'=>$ask_url, \'creatime\'=>date(\'Y-m-d H:i:s\',time()) ); $result = M(\'log_ip_ask\')->data($add_data)->add(); if($result===false){ E(\'写入记录失败!\'); } return true; }
/** * 获取客户端IP地址 * @param integer $type 返回类型 0 返回IP地址 1 返回IPV4地址数字 * @param boolean $adv 是否进行高级模式获取(有可能被伪装) * @return mixed */ private function sys_get_client_ip($type = 0,$adv=false) { $type = $type ? 1 : 0; static $ip = NULL; if ($ip !== NULL) return $ip[$type]; if($adv){ if (isset($_SERVER[\'HTTP_X_FORWARDED_FOR\'])) { $arr = explode(\',\', $_SERVER[\'HTTP_X_FORWARDED_FOR\']); $pos = array_search(\'unknown\',$arr); if(false !== $pos) unset($arr[$pos]); $ip = trim($arr[0]); }elseif (isset($_SERVER[\'HTTP_CLIENT_IP\'])) { $ip = $_SERVER[\'HTTP_CLIENT_IP\']; }elseif (isset($_SERVER[\'REMOTE_ADDR\'])) { $ip = $_SERVER[\'REMOTE_ADDR\']; } }elseif (isset($_SERVER[\'REMOTE_ADDR\'])) { $ip = $_SERVER[\'REMOTE_ADDR\']; } // IP地址合法验证 $long = sprintf("%u",ip2long($ip)); $ip = $long ? array($ip, $long) : array(\'0.0.0.0\', 0); return $ip[$type]; } /** * @desc php获取当前访问的完整url地址 * @return string */ private function sys_GetCurUrl() { $url = \'http://\'; if (isset ( $_SERVER [\'HTTPS\'] ) && $_SERVER [\'HTTPS\'] == \'on\') { $url = \'https://\'; } if ($_SERVER [\'SERVER_PORT\'] != \'80\') { $url .= $_SERVER [\'HTTP_HOST\'] . \':\' . $_SERVER [\'SERVER_PORT\'] . $_SERVER [\'REQUEST_URI\']; } else { $url .= $_SERVER [\'HTTP_HOST\'] . $_SERVER [\'REQUEST_URI\']; } return $url; }
非法ip限制访问,此处的限制一般用在服务器间的接口调用做此限制
// 允许访问的IP列表 private $ip_allow = array( \'111.11.111.111\', // 局域网ip \'111.11.111.112\', // 任务服务器 \'111.11.111.113\', // 代理IP ); /** * @desc 非法IP限制访问 * @param array $config * @return bool */ private function illegalip(){ if(!$this->ip_limit){ return true; } $remote_ip = get_client_ip(); if(in_array($remote_ip, $ip_allow)){ return true; } return false; }