PHP接口签名如何设计

wen PHP项目 2

本文目录导读:

PHP接口签名如何设计

  1. 核心设计思路
  2. 完整实现方案
  3. 推荐使用AES+RSA混合加密方案(高安全)
  4. 关键安全措施
  5. 完整调用流程
  6. 注意事项
  7. 更高级的方案(开发量较大)

在PHP中设计接口签名,主要目的是确保请求的完整性、身份认证和防篡改,这里我给你一个完整且实战的签名设计方案。

核心设计思路

签名机制核心是:客户端用私钥对请求参数进行签名,服务端用公钥/秘钥验证签名

完整实现方案

签名规则定义

<?php
/**
 * API签名工具类
 * 签名规则:按参数名ASCII码升序排列,拼接成字符串,加上时间戳和随机数,用MD5/SHA256加密
 */
class ApiSign
{
    // 签名秘钥(生产环境应放在配置文件中)
    private static $secretKey = 'your-secret-key-here';
    /**
     * 生成签名
     * @param array $params 业务参数
     * @param string $timestamp 时间戳
     * @param string $nonce 随机数
     * @return string 签名字符串
     */
    public static function generateSign($params, $timestamp, $nonce)
    {
        // 1. 过滤空值和签名参数
        $params = array_filter($params, function($value) {
            return $value !== '' && $value !== null; // 保留 0 和 false
        });
        // 2. 添加公共参数
        $params['timestamp'] = $timestamp;
        $params['nonce'] = $nonce;
        // 3. 按KEY升序排序
        ksort($params);
        // 4. 拼接字符串
        $string = '';
        foreach ($params as $key => $value) {
            $string .= $key . '=' . $value . '&';
        }
        // 5. 去除最后一个&并拼接秘钥
        $string = rtrim($string, '&') . self::$secretKey;
        // 6. 生成签名(可以使用MD5或SHA256)
        return md5($string);
    }
    /**
     * 验证签名
     * @param array $params 请求参数
     * @param string $sign 客户端签名
     * @param string $timestamp 时间戳
     * @param string $nonce 随机数
     * @return bool 是否有效
     */
    public static function verifySign($params, $sign, $timestamp, $nonce)
    {
        // 1. 时间戳校验(防止重放攻击)
        $currentTime = time();
        $requestTime = intval($timestamp);
        $expireTime = 300; // 5分钟有效
        if (abs($currentTime - $requestTime) > $expireTime) {
            return false; // 请求过期
        }
        // 2. 验证nonce(防止重放攻击)
        if (!self::checkNonce($nonce)) {
            return false;
        }
        // 3. 计算签名
        $expectedSign = self::generateSign($params, $timestamp, $nonce);
        // 4. 比较签名(使用hash_equals防止时序攻击)
        return hash_equals($expectedSign, $sign);
    }
    /**
     * 检查随机数是否已经使用
     * 实际项目中可以存Redis
     */
    private static function checkNonce($nonce)
    {
        // 示例:检查Redis中是否存在
        // if (Redis::get('nonce:' . $nonce)) return false;
        // Redis::setex('nonce:' . $nonce, 300, 1);
        return true; // 简化为始终通过
    }
}

客户端签名生成示例

<?php
/**
 * 客户端如何调用有签名的API
 */
class ApiClient
{
    private $baseUrl = 'https://api.example.com/';
    /**
     * 发送带签名的请求
     */
    public function sendRequest($endpoint, $params = [])
    {
        // 生成时间戳和随机数
        $timestamp = time();
        $nonce = $this->generateNonce();
        // 生成签名
        $sign = ApiSign::generateSign($params, $timestamp, $nonce);
        // 添加签名参数到请求
        $params['timestamp'] = $timestamp;
        $params['nonce'] = $nonce;
        $params['sign'] = $sign;
        // 发送请求
        $ch = curl_init($this->baseUrl . $endpoint);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $response = curl_exec($ch);
        curl_close($ch);
        return json_decode($response, true);
    }
    /**
     * 生成随机数
     */
    private function generateNonce()
    {
        return md5(uniqid(mt_rand(), true));
    }
}

服务端验证中间件

<?php
/**
 * API签名验证中间件
 */
class SignMiddleware
{
    public function handle($request, $next)
    {
        // 获取请求参数
        $params = $request->all();
        $sign = $params['sign'] ?? '';
        $timestamp = $params['timestamp'] ?? '';
        $nonce = $params['nonce'] ?? '';
        // 移除签名参数后验证
        unset($params['sign']);
        // 验证签名
        $isValid = ApiSign::verifySign(
            $params,
            $sign,
            $timestamp,
            $nonce
        );
        if (!$isValid) {
            return response()->json([
                'code' => 401,
                'message' => '签名验证失败'
            ], 401);
        }
        return $next($request);
    }
}

推荐使用AES+RSA混合加密方案(高安全)

对于金融、支付等安全要求高的场景,建议使用AES+RSA混合加密

<?php
/**
 * 高级签名方案:AES+RSA混合加密
 */
class AdvancedApiSecurity
{
    // RSA公钥(客户端持有)
    private $rsaPublicKey;
    // RSA私钥(服务端持有)
    private $rsaPrivateKey;
    /**
     * 生成请求签名(服务端)
     */
    public function generateSecureRequest($params, $aesKey)
    {
        // 1. 使用AES加密业务数据
        $aes = new AesEncryption($aesKey);
        $encryptedData = $aes->encrypt(json_encode($params));
        // 2. 生成签名
        $timestamp = time();
        $nonce = $this->generateNonce();
        $signStr = $encryptedData . $timestamp . $nonce . $aesKey;
        $sign = md5($signStr);
        return [
            'data' => $encryptedData,
            'timestamp' => $timestamp,
            'nonce' => $nonce,
            'sign' => $sign
        ];
    }
    /**
     * 验证请求签名(客户端)
     */
    public function verifySecureRequest($requestData, $aesKey)
    {
        $data = $requestData['data'];
        $timestamp = $requestData['timestamp'];
        $nonce = $requestData['nonce'];
        $sign = $requestData['sign'];
        // 验证时间戳(防止重放)
        if (abs(time() - intval($timestamp)) > 300) {
            throw new Exception('请求已过期');
        }
        // 验证签名
        $signStr = $data . $timestamp . $nonce . $aesKey;
        if (!hash_equals(md5($signStr), $sign)) {
            throw new Exception('签名验证失败');
        }
        // 解密数据
        $aes = new AesEncryption($aesKey);
        return json_decode($aes->decrypt($data), true);
    }
}

关键安全措施

防止重放攻击

// 建议用Redis存储已使用的nonce
public function checkAndStoreNonce($nonce)
{
    $key = 'api_nonce:' . $nonce;
    if (Redis::exists($key)) {
        return false; // 重放攻击
    }
    Redis::setex($key, 300, 1); // 5分钟有效期
    return true;
}

防止时序攻击

// 使用安全的比较函数
if (!hash_equals($expectedSign, $sign)) {
    // 签名不匹配
}

动态Secret Key

// 为每个应用/用户分配独特的secret key
class DynamicSecretKey
{
    public static function getSecretKey($appId)
    {
        // 从数据库或Redis获取该应用的secret key
        $secret = \App\Models\ApiClient::where('app_id', $appId)->value('secret_key');
        return $secret;
    }
}

IP限制和白名单

class IpRestrict
{
    public static function checkIp($ip, $appId)
    {
        // 从数据库获取该应用的白名单IP列表
        $allowedIps = ['123.56.78.90', '123.56.78.91'];
        return in_array($ip, $allowedIps);
    }
}

完整调用流程

客户端                       服务端
  |                            |
  | 1. 准备业务参数             |
  | 2. 生成时间戳、nonce         |
  | 3. 生成签名                 |
  | 4. 发送请求  |-------------->| 5. 接收请求
  |              |              | 6. 验证时间戳(防重放)
  |              |              | 7. 验证nonce
  |              |              | 8. 重新计算签名
  |              |              | 9. 比较签名是否一致
  |              |              | 10. 处理业务逻辑
  | 11. 接收响应 |<-------------| 11. 返回结果
  |              |

注意事项

  1. 参数传递顺序:不要依赖参数的顺序,始终按Key排序
  2. 数组参数处理:如果包含嵌套数组,需要递归处理
  3. 特殊字符:URL编码时注意 + 和 空格 的处理
  4. 多语言兼容:不同语言的处理方式可能略有差异,建议写详细的签名规则文档
  5. 日志记录:建议记录签名验证失败的信息,便于排查问题

更高级的方案(开发量较大)

  • OAuth 2.0:用于第三方授权
  • JWT:无状态认证,适合分布式系统
  • 国密SM2/SM3:国内金融级要求

建议根据你的具体业务场景选择适合的安全级别,如果是一般的应用,使用第一种方案就够了;如果涉及资金交易,建议使用第二种方案。

抱歉,评论功能暂时关闭!