签名生成说明

B项目需要调用A项目的接口,由A项目为B项目分配 AccessKeySecretKey,用于接口加密,确保不易被穷举,生成算法不易被猜测。

最终需要确保包含签名的参数只能被有效的请求一次,重复请求则视为无效参数;并且设定参数有效时长(例如5分钟),超时则视为无效参数。

AccessKey 和 SecretKey分配:

测试环境:
ACCESS_KEY = test_access
SECRET_KEY = test_secret

正式环境:(另行配置)

假设A项目和B项目通过json格式传递参数,在PHP中对请求的json参数转化为数组,然后对原本的请求参数追加如下字段值:

  • AccessKey:已分配的请求key,固定值;
  • timestamp:当前毫秒时间戳;
  • nonce:唯一随机10位字符串,15分钟内不允许重复;

例如,原本的请求参数 $params 为:

Array
(
    [ToUserName] => wxdd5624bd15b1691a
    [FromUserName] => sys
    [CreateTime] => 1717554600
    [MsgType] => event
    [Event] => sys_approval_change
    [AgentID] => 1000043
)

$params 追加 AccessKeytimestampnonce 之后:

Array
(
    [ToUserName] => wxdd5624bd15b1691a
    [FromUserName] => sys
    [CreateTime] => 1717554600
    [MsgType] => event
    [Event] => sys_approval_change
    [AgentID] => 1000043
    [AccessKey] => test_access
    [timestamp] => 1717659814771
    [nonce] => 6bc6f34969
)

$params 的 key 值按照字母升序排列(PHP中的 ksort 函数):

Array
(
    [AccessKey] => test_access
    [AgentID] => 1000043
    [CreateTime] => 1717554600
    [Event] => sys_approval_change
    [FromUserName] => sys
    [MsgType] => event
    [ToUserName] => wxdd5624bd15b1691a
    [nonce] => 756c577626
    [timestamp] => 1717659831355
)

然后,将上述参数赋给一个临时的变量(例如:$tmp_params),并且拼接 SecretKey,然后整体json_encode,再次md5之后,得到sign值,代码如下:

$sign = md5(json_encode($tmp_params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

sign值 追加到 $params 参数中(注意:是$params参数,不是 $tmp_params ),最终参数如下:

Array
(
    [AccessKey] => test_access
    [AgentID] => 1000043
    [CreateTime] => 1717554600
    [Event] => sys_approval_change
    [FromUserName] => sys
    [MsgType] => event
    [ToUserName] => wxdd5624bd15b1691a
    [nonce] => 137c128684
    [timestamp] => 1717660145228
    [sign] => ff0ea47d561eb2d9735771f0bc85ad33
)

将上述参数转化为json后作为最终的请求参数:

{
    "AccessKey": "test_access",
    "AgentID": "1000043",
    "CreateTime": "1717554600",
    "Event": "sys_approval_change",
    "FromUserName": "sys",
    "MsgType": "event",
    "ToUserName": "wxdd5624bd15b1691a",
    "nonce": "fb212b7327",
    "timestamp": 1717660335729,
    "sign": "9e5321b10ddc975b89a228e94d8e5f04"
}

签名生成示例代码

public function createSign()
{
    $mock_json = '{
        "ToUserName": "wxdd5624bd15b1691a",
        "FromUserName": "sys",
        "CreateTime": "1717554600",
        "MsgType": "event",
        "Event": "sys_approval_change",
        "AgentID": "1000043"
    }';
    $params = json_decode($mock_json, true);

    //对原本的请求参数追加如下字段值:
    $params['AccessKey'] = 'test_access'; //已分配的请求key,固定值
    $params['timestamp'] = intval(microtime(true) * 1000); //当前毫秒时间戳
    $params['nonce'] = substr(uniqid(), -6) . rand(1000, 9999); //唯一随机10位字符串,15分钟内不允许重复

    //按照上述所有请求参数的key值的字母升序排列(PHP中的 `ksort` 函数):
    ksort($params);

    //然后,将上述参数赋给一个临时的变量,并且拼接 SecretKey, 然后整体json_encode,再次md5之后,得到sign值
    $tmp_params = $params;
    $tmp_params['SecretKey'] = 'test_secret';
    $sign = md5(json_encode($tmp_params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

    //将 sign值 追加到 $params 参数中(注意:是$params参数,不是 $tmp_params )
    $params['sign'] = $sign;

    //将上述参数转化为json后作为最终的请求参数:
    echo json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}

签名校验示例代码

<?php

class Demo
{
    //时间常量
    const TIME_OUT = 300; //超时时间  5分钟
    const NONCE_INTERVAL = 900;   //允许nonce时间间隔   15分钟

    /**
     * 签名验证
     * @param $params array 客户端请求来的原本的参数数组
     * @return array
     * @throws \Exception
     */
    public function checkSign($params)
    {
        $request_params = $params;
        if (empty($params['timestamp']) || empty($params['nonce']) || empty($params['sign'])) {
            throw new \Exception('签名基础参数校验失败', 201);
        }

        //校验超时
        $timestamp = intval($params['timestamp'] / 1000);
        if (abs(time() - $timestamp) > self::TIME_OUT) {
            throw new \Exception('请求参数已超时', 201);
        }

		//从配置文件中读取ACCESS_KEY和SECRET_KEY
        $access_key = env('ACCESS_KEY');
        $secret_key = env('SECRET_KEY');
        if (empty($access_key) || empty($secret_key)) {
            throw new \Exception('NEW_CRM_REQUEST配置异常', 201);
        }
        if ($access_key != $params['AccessKey']) {
            throw new \Exception('无效的AccessKey', 201);
        }

        $nonce_key = 'test_nonce:' . $params['timestamp'] . '_' . $params['nonce'];
        $exist_nonce = RedisUtils::init()->get($nonce_key);
        if ($exist_nonce) {
            throw new \Exception('无效的nonce值', 201);
        }

        $sign = $params['sign'];
        unset($params['sign']);
        ksort($params);
        $params['SecretKey'] = $secret_key;
        $params_json = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        $params_sign = md5($params_json);
        if ($params_sign != $sign) {
            //todo 写入错误log 或 发送报警信息
			
			//todo 校验频繁请求失败的IP,可以考虑将这些IP加入黑名单
            throw new \Exception('签名校验失败', 201);
        }

        RedisUtils::init()->set($nonce_key, 1, self::NONCE_INTERVAL);
        unset($params['AccessKey']);
        unset($params['SecretKey']);
        unset($params['nonce']);
        unset($params['timestamp']);

        return $params;
    }
}

最终效果,同样的请求参数如果被抓包,再次请求就会失败:
在这里插入图片描述

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部