php接口安全设计实现
接口的安全性主要围绕Token、Timestamp和Sign三个机制展开设计,保证接口的数据不会被篡改和重复调用,下面具体来看:
(1)Token授权机制:(Token是客户端访问服务端的凭证)–用户使用用户名密码登录后服务器给客户端返回一个Token(通常是UUID),并将Token-UserId以键值对的形式存放在缓存服务器中。服务端接收到请求后进行Token验证,如果Token不存在,说明请求无效。
(2)时间戳超时机制:(签名机制保证了数据不会被篡改)用户每次请求都带上当前时间的时间戳timestamp,服务端接收到timestamp后跟当前时间进行比对,如果时间差大于一定时间(比如5分钟),则认为该请求失效。时间戳超时机制是防御DOS攻击的有效手段。
(3)签名机制:将 Token 和 时间戳 加上其他请求参数再用MD5或SHA-1算法(可根据情况加点盐)加密,加密后的数据就是本次请求的签名sign,服务端接收到请求后以同样的算法得到签名,并跟当前的签名进行比对,如果不一样,说明参数被更改过,直接返回错误标识。
简单的实现了接口校验:登陆获取到token
/**
* 模拟登陆 分配随机token
* @route('login')
*/
public function login()
{
$data = $this->request->param();
if( $data['name']=="luozhengbo" && $data['pass'] == "123456"){
$id=1;
$token= Cache::get('token'.$id);
if(!$token){
//15c3463ffb65d9
$token =$id.uniqid();
Cache::set('token'.$id,$token,'3*60');
}
return json($token);
}else{
return json('用户或密码不正确');
}
}
用php模拟客服端,登陆成功获得token 并生成随机数、时间戳、生成sign ,生成这些数据之后请求接口时,带上相应的参数过去进行鉴权,鉴权验签部分用的工具就不贴代码了。
//随机生成字符串
private function createNonceStr($length = 8) {
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
/**
* @param $timeStamp 时间戳
* @param $randomStr 随机字符串
* @return string 返回签名
* @route('checklogin')
*/
public function createSign($timeStamp='',$token='',$randomStr=''){
$arr['timeStamp'] = $timeStamp;
$arr['randomStr'] = $randomStr;
$arr['token'] = $token;
//按照首字母大小写顺序排序
sort($arr,SORT_STRING);
//拼接成字符串
$str = implode($arr);
//进行加密
$signature = sha1($str);
$signature = md5($signature);
//转换成大写
$signature = strtolower($signature);
return $signature;
}
/**
* @return mixed
* @route('userlogin')
*/
public function login()
{
$token = json_decode(httpGet("10.20.1.172/index.php/login/name/luozhengbo/pass/123456"),true);
return $token;
}
/**
* @route('getradmon')
*/
public function getDataFromServer(){
//时间戳
$timeStamp = time();
//随机数
$randomStr = $this -> createNonceStr();
//生成签名
$token = $this->login();
$signature = $this -> createSign($timeStamp,$token,$randomStr);
dump($token);
dump($timeStamp);
dump($randomStr);
dump($signature);
die;
}
接下来就是服务端鉴权代码,服务端拿到timestamp、token、sign、randomStr等参数进行验证过程
namespace app\index\controller;
use think\facade\Cache;
use think\facade\Config;
use think\Controller;
use think\facade\exception;
use think\Db;
class common extends controller
{
//限制访问
protected $ip_limit =array(
'10.20.1.60',
'10.20.1.62',
'10.20.1.61',
);
/**
* 签名检验
* @route('auth')
*
*/
public function checkAuth($token,$randomStr,$timestamp,$sign)
{
//参数判断
if(empty($token)){
exception('token不能为空','500');
}
if(empty($timestamp)){
exception('时间戳不能为空','500');
}
if(empty($randomStr)){
exception('随机字符串不能为空','500');
}
if(empty($sign)){
exception('签名不能为空','500');
}
//检验token
$tokenCheck = Cache::get('1token');
if($tokenCheck != $token ){
exception('token已经过期','500');
}
//时间校验
$exprie = Config::get('app.expire');
$timestamp1 = $timestamp+$exprie;
if( $timestamp1<time() ){//过期
exception('请求已经过期','500');
}
if(!$this->illegalip()){
return json('ip限制访问');
}
//按规则拼接为字符串
$str = $this->arithmetic($timestamp,$token,$randomStr);
if($str != $sign){
exception('验签错误','500');
}
if(!$this->ask_count()){
return json('验签error');
}
return json('验签ok');
}
/**
* @param $timeStamp 时间戳
* @param $randomStr 随机字符串
* @return string 返回签名
*/
public function arithmetic($timeStamp,$token,$randomStr){
$arr['timeStamp'] = $timeStamp;
$arr['randomStr'] = $randomStr;
$arr['token'] = $token;
//按照首字母大小写顺序排序
sort($arr,SORT_STRING);
//拼接成字符串
$str = implode($arr);
//进行加密
$signature = sha1($str);
$signature = md5($signature);
//转换成小写
$signature = strtolower($signature);
return $signature;
}
/**
* @desc 限制请求接口次数
* @return bool
*/
private function ask_count(){
$client_ip = getClientIp();
$ask_url = getUrl();
//限制次数
$limit_num = Config::get('api_ask_limit');
//有效时间内,单位:秒
$limit_time = Config::get('api_ask_time');
$now_time = time();
$valid_time = $now_time - $limit_time;
$valid_time = date('Y-m-d H:i:s',$valid_time);
$now_time = date('Y-m-d H:i:s',$now_time);
$ipwhere['ip_name'] = $client_ip;
$ipwhere['ask_url'] = $ask_url;
$check_result = Db::name('log_ip_ask')
->whereBetweenTime('creatime',$valid_time,$now_time)
->where($ipwhere)
->count();
if($check_result !=='0'){
if($check_result >= $limit_num){
exception('已经超出了限制次数');
}
}
//执行插入
$add_data = array(
'ip_name'=>$client_ip,
'ask_url'=>$ask_url,
'creatime'=>date('Y-m-d H:i:s',time())
);
$result = Db::name('log_ip_ask')->insert($add_data);
if($result===false){
exception('已经超出了限制次数');
}
return true;
}
/**
* @desc 非法IP限制访问
* @param array $config
* @return bool
*/
private function illegalip(){
if( !$this->ip_limit ){
return true;
}
$remote_ip = getClientIp();
if(in_array($remote_ip, $this->ip_limit)){
return false;
}
return true;
}
/**
* 模拟登陆 分配随机token
* @route('login')
*/
public function login()
{
$data = $this->request->param();
if( $data['name']=="luozhengbo" && $data['pass'] == "123456"){
$id=1;
$token= Cache::get('token'.$id);
if(!$token){
//15c3463ffb65d9
$token =$id.uniqid();
Cache::set('token'.$id,$token,'3*60');
}
return json($token);
}else{
return json('用户或密码不正确');
}
}
}
最后还有几个公用的方法和配置
'expire'=>5*6000000,//api有效期
'api_ask_limit'=>10,//api访问次数
'api_ask_time'=>60,
common中的函数
/**
* 获取客户端IP地址
* @param integer $type 返回类型 0 返回IP地址 1 返回IPV4地址数字
* @param boolean $adv 是否进行高级模式获取(有可能被伪装)
* @return mixed
*/
function getClientIp($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
*/
function getUrl() {
$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;
}
/**
*
* curl模拟get请求
*/
function httpGet($url){
$curl = curl_init();
//需要请求的是哪个地址
curl_setopt($curl,CURLOPT_URL,$url);
//表示把请求的数据已文件流的方式输出到变量中
curl_setopt($curl,CURLOPT_RETURNTRANSFER,1);
$result = curl_exec($curl);
curl_close($curl);
return $result;
}
以上还有可改进的地方,限制ip、次数等。大家有什么好的建议可以说一下。
参考:https://www.cnblogs.com/zouke1220/p/9394356.html