PHP开发API接口签名验证
简介在接口开发中处于对安全的考虑通常会对数据进行加密,防止中途数据被篡改。所以本文主要介绍下一种通用的接口数据签名验证方法。
在对外提供接口时,我们一定要注意到数据的安全问题。如果可以建议使用HTTPS,但是在接口安全方面,还是建议大家对接口数据进行数据签名,防止中途数据被串改。签名的主要思想如下:
1、首先要定义如下三个字段:
当前时间戳:防止接口一直被刷
签名字段:主要是服务端用来进行对比数据是否一致
签名场景:便于扩展,如果不想使用场景,可以用公共的签名
必须将当前时间戳和签名场景参数追加到传递进来的参数中。
2、对所有参数按照键进行升序排列
3、将第二步排列好的参数按照key=value&key=value进行拼接
4、将第三步拼接好的参数与秘钥拼接
5、将第四步的字符串进行md5进行加密,生成对应的签名字符串
6、将第五步的签名数据追加到传递进来的参数中,然后返回出去
7、将第六步的数据来进行签名对比
代码如下(勿喷代码质量,主要是思想,谢谢):
<?php
/**
* 数据签名类
*/
class DataSign
{
/**
* 是否启用验证
* @var
*/
private $_enable;
/**
* 签名秘钥
* @var string
*/
private $_secrete;
/**
* 过期时间 空字符串,null、0、false,[],都表示不验证时间,单位为秒
* @var int
*/
private $_expireTime;
/**
* 签名场景
* @var string
*/
private $_signScene;
/**
* 签名场景KEY,需要根据自己的使用更改成相应字段
* @var string
*/
const SIGN_SCENE_KEY = 'custom_sign_scene';
/**
* 签名中的时间KEY,需要根据自己的使用更改成相应字段
* @var string
*/
const TIMESTAMP_KEY = 'custom_sign_timestamp';
/**
* 签名中的签名KEY,需要根据自己的使用更改成相应字段
* @var string
*/
const SIGN_KEY = 'custom_sign_key';
/**
* 配置信息,这里主要定义一个全局的配置,然后可以对单独场景进行定义处理,方便后续的扩展
*
* @var array
*/
private $config = [
//是否启用验证
'enable' => true,
//全局公共秘钥
'secret' => 'wPPdQsM6x4#M8QboUw%lSMy&p2ckvWOLOJuuX7qVhlARnBRGSB#u8GCoChW&@g@s',
//全局过期时间
'expire_time' => 0,//签名过期时间,空字符串,null、0、false,[],都表示不验证时间
'scene' => [//使用场景
//队列签名
'api' => [
'enable' => false,
'secret' => '2%Gw%bzSOy7tYzht*ayPju1K9Mk#XUilZyULTco3lRsHgttQv7tD1AvmAXu@CFXS',
'expire_time' => 3600,
],
],
];
/**
* DataSignService constructor.
* @param string $scene 场景
* @throws Exception
*/
public function __construct($scene = '')
{
$scene = $scene ? strtolower($scene) : '';
if ($scene && isset($this->config['scene'][$scene]['secret']) && isset($this->config['scene'][$scene]['expire_time'])) {
$this->_secrete = $this->config['scene'][$scene]['secret'];
$this->_expireTime = $this->config['scene'][$scene]['expire_time'];
$this->_enable = $this->config['scene'][$scene]['enable'];
$this->_signScene = $scene;
} else {
$this->_secrete = $this->config['secret'];
$this->_expireTime = $this->config['expire_time'];
$this->_enable = $this->config['enable'];
}
if (!is_int($this->_expireTime)) {
$this->throwException('过期时间必须是整数');
}
if ($this->_expireTime < 0) {
$this->throwException('过期时间必须是大于0的正整数');
}
}
/**
* 获取签名数据
* @param $data
* @return string
*/
public function getSign($data)
{
//添加当前时间到数据中
$data[self::TIMESTAMP_KEY] = strval(time());
//场景处理
if ($this->_signScene) {
$data[self::SIGN_SCENE_KEY] = $this->_signScene;
}
//获取签名,添加签名数据到数据中
$data[self::SIGN_KEY] = $this->sign($data);
//返回组装后的数据
return $data;
}
/**
* 验证签名数据
* @param $data
* @return bool
* @throws Exception
*/
public function checkSign($data)
{
//如果不验证,直接返回成功
if (!$this->_enable) {
return true;
}
//验证两个字段是否存在
if (!isset($data[self::SIGN_KEY])) {
$this->throwException('数据缺少sign参数');
}
if ($this->_expireTime && !isset($data[self::TIMESTAMP_KEY])) {
$this->throwException('数据缺少timestamp参数');
}
//获取时间并判断时间是否过期
if ($this->_expireTime && time() - $data[self::TIMESTAMP_KEY] > $this->_expireTime) {
$this->throwException('数据已过期');
}
//获取签名,并取消数据中的签名字段
$sourceSign = $data[self::SIGN_KEY];
unset($data[self::SIGN_KEY]);
$sign = $this->sign($data);
if ($sourceSign != $sign) {
$this->addSignErrorLog($data, $sourceSign, $sign);
$this->throwException('签名失败');
}
return true;
}
/**
* 签名处理
* @param $data
* @return string
*/
private function sign($data)
{
ksort($data);
$paramStr = http_build_query($data);
return md5($paramStr . $this->_secrete);
}
/**
* 签名错误日志
* @param $data
* @param $sourceSign
* @param $sign
*/
private function addSignErrorLog($data, $sourceSign, $sign)
{
//写入日志信息 TODO 可以记录到文件或数据库中
}
/**
* @param $message
* @throws Exception
*/
private function throwException($message)
{
throw new Exception($message);
}
}
$data = ['name' => 'zhang', 'age' => 10];
/**
* 使用全局
*/
$result = (new DataSign())->getSign($data);
print_r($result);
$result = (new DataSign())->checkSign($result);
print_r($result);
/**
* 使用单独场景
*/
$result = (new DataSign('api'))->getSign($data);
print_r($result);
$result = (new DataSign('api'))->checkSign($result);
print_r($result);