PHP做APP接口时,如何保证接口的安全性?

1、当用户登录APP时,使用https协议调用后台相关接口,服务器端根据用户名和密码时生成一个access_key,并将access_key保存在session中,将生成的access_key和session_id返回给APP端。

2、APP端将接收到的access_key和session_id保存起来

3、当APP端调用接口传输数据时,将所传数据和access_key使用加密算法生成签名signature,并将signature和session_id一起发送给服务器端。

4、服务器端接收到数据时,使用session_id从session中获取对应的access_key,将access_key和接收到的数据使用同一加密算法生成对应signature,如果生成的签名和接收到的signature相同时,则表明数据合法

php接口安全设计浅谈
Token授权机制
时间戳超时机制:
签名机制:
拒绝重复调用:客户端第一次访问时,将签名sign存放到缓存服务器中,超时时间设定为跟时间戳的超时时间一致,二者时间一致可以保证无论在timestamp限定时间内还是外 URL都只能访问一次。
非法ip限制访问,此处的限制一般用在服务器间的接口调用做此限制 白名单
https访问

接口的安全性主要围绕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;

}

复制代码

参考链接:https://www.jianshu.com/p/c6518a8f4040

App开放接口api安全性—Token签名sign的设计与实现
前言

在app开放接口api的设计中,避免不了的就是安全性问题,因为大多数接口涉及到用户的个人信息以及一些敏感的数据,所以对这些 接口需要进行身份的认证,那么这就需要用户提供一些信息,比如用户名密码等,但是为了安全起见让用户暴露的明文密码次数越少越好,我们一般在web项目 中,大多数采用保存的session中,然后在存一份到cookie中,来保持用户的回话有效性。但是在app提供的开放接口中,后端服务器在用户登录后 如何去验证和维护用户的登陆有效性呢,以下是参考项目中设计的解决方案,其原理和大多数开放接口安全验证一样,如淘宝的开放接口token验证,微信开发 平台token验证都是同理。

签名设计
对于敏感的api接口,需使用https协议

https是在http超文本传输协议加入SSL层,它在网络间通信是加密的,所以需要加密证书。

https协议需要ca证书,一般需要交费。

签名的设计

原理:用户登录后向服务器提供用户认证信息(如账户和密码),服务器认证完后给客户端返回一个Token令牌,用户再次获取信息时,带上此令牌,如果令牌正取,则返回数据。对于获取Token信息后,访问用户相关接口,客户端请求的url需要带上如下参数:

时间戳:timestamp

Token令牌:token

然后将所有用户请求的参数按照字母排序(包括timestamp,token),然后更具MD5加密(可以加点盐),全部大写,生成sign签名,这就是 所说的url签名算法。然后登陆后每次调用用户信息时,带上sign,timestamp,token参数。

例如:原请求https://www.andy.cn/api/user/update/info.shtml?city=北京 (post和get都一样,对所有参数排序加密)

加上时间戳和token

https://www.andy.cn/api/user/update/info.shtml?city=北京×tamp=12445323134&token=wefkfjdskfjewfjkjfdfnc 然后更具url参数生成sign

最终的请求如

https://www.andy.cn /api/user/update/info.shtml?city=北京×tamp=12445323134& token=wefkfjdskfjewfjkjfdfnc&sign=FDK2434JKJFD334FDF2
其最终的原理是减小明文的暴露次数;保证数据安全的访问。

具体实现如下:

  1. api请求客户端想服务器端一次发送用用户认证信息(用户名和密码),服务器端请求到改请求后,验证用户信息是否正确。

如果正确:则返回一个唯一不重复的字符串(一般为UUID),然后在Redis(任意缓存服务器)中维护Token----Uid的用户信息关系,以便其他api对token的校验。

如果错误:则返回错误码。

2.服务器设计一个url请求拦截规则

(1)判断是否包含timestamp,token,sign参数,如果不含有返回错误码。

(2)判断服务器接到请求的时间和参数中的时间戳是否相差很长一段时间(时间自定义如半个小时),如果超过则说明该 url已经过期(如果url被盗,他改变了时间戳,但是会导致sign签名不相等)。

(3)判断token是否有效,根据请求过来的token,查询redis缓存中的uid,如果获取不到这说明该token已过期。

(4)根据用户请求的url参数,服务器端按照同样的规则生成sign签名,对比签名看是否相等,相等则放行。(自然url签名 也无法100%保证其安全,也可以通过公钥AES对数据和url加密,但这样如果无法确保公钥丢失,所以签名只是很大程 度上保证安全)。

(5)此url拦截只需对获取身份认证的url放行(如登陆url),剩余所有的url都需拦截。

3.Token和Uid关系维护

对于用户登录我们需要创建token--uid的关系,用户退出时需要需删除token--uid的关系。

签名实现

获取全部请求参数
复制代码

String sign = request.getParameter("sign");

Enumeration<?> pNames =  request.getParameterNames();

Map<String, Object> params = new HashMap<String, Object>();

while (pNames.hasMoreElements()) {

String pName = (String) pNames.nextElement();

if("sign".equals(pName))continue;

Object pValue = request.getParameter(pName);

params.put(pName, pValue);

}

复制代码

生成签名

复制代码

public static String createSign(Map<String, String> params, boolean encode)

throws UnsupportedEncodingException {

Set keysSet = params.keySet();

Object[] keys = keysSet.toArray();

Arrays.sort(keys);

StringBuffer temp = new StringBuffer();

boolean first = true;

for (Object key : keys) {

if (first) {

first = false;

} else {

temp.append("&");

}

temp.append(key).append("=");

Object value = params.get(key);

String valueString = "";

if (null != value) {

valueString = String.valueOf(value);

}

if (encode) {

temp.append(URLEncoder.encode(valueString, "UTF-8"));

} else {

temp.append(valueString);

}

}
return MD5Utils.getMD5(temp.toString()).toUpperCase();
}