PHP开发API接口签名验证

xiaohai 2019-05-27 19:13:50 1754人围观 标签: PHP  安全 
简介在接口开发中处于对安全的考虑通常会对数据进行加密,防止中途数据被篡改。所以本文主要介绍下一种通用的接口数据签名验证方法。

在对外提供接口时,我们一定要注意到数据的安全问题。如果可以建议使用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);