PHP API 签名验证流程

wen PHP项目 1

本文目录导读:

PHP API 签名验证流程

  1. 签名验证的基本原理
  2. 标准签名验证流程
  3. 高级签名方案(更安全)
  4. 中间件实现(Laravel示例)
  5. 安全建议
  6. 完整集成示例

我来为您详细说明PHP API签名验证的完整流程。

签名验证的基本原理

核心思想

通过特定的算法将请求参数、密钥和时间戳等信息生成签名,服务端验证签名确保请求的合法性和完整性。

标准签名验证流程

客户端签名生成流程

<?php
/**
 * 客户端签名生成
 */
class ApiSignClient {
    private $appKey;
    private $appSecret;
    public function __construct($appKey, $appSecret) {
        $this->appKey = $appKey;
        $this->appSecret = $appSecret;
    }
    /**
     * 生成签名
     * @param array $params 业务参数(不包含签名)
     * @param string $timestamp 时间戳
     * @param string $nonce 随机字符串
     * @return string
     */
    public function generateSign($params, $timestamp, $nonce) {
        // 1. 加入公共参数
        $params['app_key'] = $this->appKey;
        $params['timestamp'] = $timestamp;
        $params['nonce'] = $nonce;
        // 2. 按key排序
        ksort($params);
        // 3. 拼接字符串
        $str = urldecode(http_build_query($params));
        // 4. 加入密钥并加密
        $sign = md5($str . $this->appSecret);
        return $sign;
    }
    /**
     * 生成请求参数
     */
    public function createRequestParams($businessParams) {
        $timestamp = time();
        $nonce = $this->generateNonce(16);
        $params = array_merge($businessParams, [
            'app_key' => $this->appKey,
            'timestamp' => $timestamp,
            'nonce' => $nonce
        ]);
        $params['sign'] = $this->generateSign($businessParams, $timestamp, $nonce);
        return $params;
    }
    /**
     * 生成随机字符串
     */
    private function generateNonce($length = 16) {
        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        $nonce = '';
        for ($i = 0; $i < $length; $i++) {
            $nonce .= $chars[rand(0, strlen($chars) - 1)];
        }
        return $nonce;
    }
}

服务端签名验证流程

<?php
/**
 * 服务端签名验证
 */
class ApiSignServer {
    private $appKey;
    private $appSecret;
    private $timeout; // 请求超时时间(秒)
    public function __construct($appKey, $appSecret, $timeout = 300) {
        $this->appKey = $appKey;
        $this->appSecret = $appSecret;
        $this->timeout = $timeout;
    }
    /**
     * 验证签名主方法
     * @param array $params 请求参数
     * @return bool|string 成功返回true,失败返回错误信息
     */
    public function verifySign($params) {
        try {
            // 1. 检查必填参数
            $this->checkRequiredParams($params);
            // 2. 验证AppKey
            if ($params['app_key'] !== $this->appKey) {
                return 'Invalid app_key';
            }
            // 3. 验证时间戳
            if (!$this->checkTimestamp($params['timestamp'])) {
                return 'Timestamp expired';
            }
            // 4. 验证Nonce(防止重放攻击)
            if (!$this->checkNonce($params['nonce'])) {
                return 'Nonce repeated';
            }
            // 5. 重新生成签名并对比
            $sign = $this->generateSign($params);
            if ($sign !== $params['sign']) {
                return 'Sign verification failed';
            }
            // 6. 记录nonce(需要在红缓存或数据库中实现)
            $this->saveNonce($params['nonce'], $this->timeout);
            return true;
        } catch (Exception $e) {
            return $e->getMessage();
        }
    }
    /**
     * 检查必填参数
     */
    private function checkRequiredParams($params) {
        $required = ['app_key', 'timestamp', 'nonce', 'sign'];
        foreach ($required as $field) {
            if (!isset($params[$field]) || empty($params[$field])) {
                throw new Exception("Missing required parameter: {$field}");
            }
        }
        // 检查业务参数是否完整(根据具体业务)
        // ...
    }
    /**
     * 验证时间戳是否过期
     */
    private function checkTimestamp($timestamp) {
        $currentTime = time();
        if ($timestamp < $currentTime - $this->timeout || $timestamp > $currentTime + $this->timeout) {
            return false;
        }
        return true;
    }
    /**
     * 检查Nonce是否重复
     */
    private function checkNonce($nonce) {
        // 实现方式1:Redis
        $redis = new Redis();
        $redis->connect('127.0.0.1', 6379);
        if ($redis->exists("api_nonce:{$nonce}")) {
            return false;
        }
        return true;
        // 实现方式2:数据库
        // $query = "SELECT COUNT(*) FROM api_nonce WHERE nonce = ?";
        // if (查询结果 > 0) return false;
        // return true;
    }
    /**
     * 保存Nonce(设置过期时间)
     */
    private function saveNonce($nonce, $timeout) {
        // Redis实现
        $redis = new Redis();
        $redis->connect('127.0.0.1', 6379);
        $redis->setex("api_nonce:{$nonce}", $timeout, time());
        // 数据库实现
        // INSERT INTO api_nonce (nonce, created_at) VALUES (?, NOW())
        // 定期清理过期记录
    }
    /**
     * 生成服务端签名
     */
    private function generateSign($params) {
        // 去除签名参数
        unset($params['sign']);
        // 按key排序
        ksort($params);
        // 拼接字符串
        $str = urldecode(http_build_query($params));
        // 加入密钥并加密
        return md5($str . $this->appSecret);
    }
}
// 使用示例
$apiSignServer = new ApiSignServer('your_app_key', 'your_app_secret');
$verifyResult = $apiSignServer->verifySign($_GET);
if ($verifyResult === true) {
    // 签名验证通过,处理业务逻辑
    echo json_encode(['code' => 200, 'message' => 'Success']);
} else {
    // 签名验证失败
    echo json_encode(['code' => 401, 'message' => $verifyResult]);
}

高级签名方案(更安全)

使用HMAC-SHA256签名

<?php
/**
 * 高级签名生成(使用HMAC-SHA256)
 */
class AdvancedApiSign {
    /**
     * 生成签名
     */
    public static function generateSign($params, $appSecret) {
        // 1. 过滤空值和签名字段
        $params = array_filter($params, function($value) {
            return $value !== '' && $value !== null && $value !== 'sign';
        });
        // 2. 按key排序
        ksort($params);
        // 3. 拼接字符串 "key1=value1&key2=value2"
        $str = http_build_query($params);
        // 4. 使用HMAC-SHA256加密
        $sign = hash_hmac('sha256', $str, $appSecret);
        return strtoupper($sign);
    }
    /**
     * 验证签名
     */
    public static function verifySign($params, $appSecret) {
        if (!isset($params['sign'])) {
            return false;
        }
        $clientSign = $params['sign'];
        unset($params['sign']);
        $serverSign = self::generateSign($params, $appSecret);
        // 使用恒等比较防止时序攻击
        return hash_equals($serverSign, $clientSign);
    }
}

更安全的签名流程(包含请求体签名)

<?php
/**
 * 完整的安全签名方案
 */
class SecureApiSign {
    private $appKey;
    private $appSecret;
    private $timestamp;
    private $nonce;
    /**
     * 完整签名流程
     */
    public function signRequest($method, $path, $body = null, $timestamp = null) {
        $timestamp = $timestamp ?? time();
        $nonce = $this->generateNonce(32);
        $params = [
            'app_key' => $this->appKey,
            'timestamp' => $timestamp,
            'nonce' => $nonce,
            'method' => strtoupper($method),
            'path' => $path
        ];
        // 如果有请求体,加入签名
        if ($body !== null) {
            $params['body'] = sha1($body);
        }
        // 生成签名
        ksort($params);
        $str = urldecode(http_build_query($params));
        $sign = hash_hmac('sha256', $str, $this->appSecret);
        return [
            'params' => $params,
            'sign' => $sign
        ];
    }
    /**
     * 验证请求
     */
    public function verifyRequest($request) {
        try {
            // 1. 验证时间戳(5分钟内有效)
            if (abs(time() - $request['timestamp']) > 300) {
                throw new Exception('Request expired');
            }
            // 2. 验证Nonce
            if (!$this->verifyNonce($request['nonce'])) {
                throw new Exception('Nonce already used');
            }
            // 3. 验证签名
            $signParams = $request;
            unset($signParams['sign']);
            ksort($signParams);
            $str = urldecode(http_build_query($signParams));
            $expectedSign = hash_hmac('sha256', $str, $this->appSecret);
            if (!hash_equals($expectedSign, $request['sign'])) {
                throw new Exception('Invalid signature');
            }
            // 4. 验证请求体
            if (isset($request['body'])) {
                $actualBodyHash = sha1(file_get_contents('php://input'));
                if ($actualBodyHash !== $request['body']) {
                    throw new Exception('Body verification failed');
                }
            }
            return true;
        } catch (Exception $e) {
            error_log("Signature verification failed: " . $e->getMessage());
            return false;
        }
    }
    private function generateNonce($length) {
        return bin2hex(random_bytes($length / 2));
    }
}

中间件实现(Laravel示例)

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class VerifyApiSignature
{
    public function handle(Request $request, Closure $next)
    {
        $signature = $request->header('X-Signature');
        $timestamp = $request->header('X-Timestamp');
        $nonce = $request->header('X-Nonce');
        if (!$signature || !$timestamp || !$nonce) {
            return response()->json(['error' => 'Missing signature parameters'], 401);
        }
        // 获取API密钥
        $apiKey = $request->header('X-Api-Key');
        $appSecret = $this->getSecretByKey($apiKey);
        if (!$appSecret) {
            return response()->json(['error' => 'Invalid API key'], 401);
        }
        // 验证时间戳
        if (abs(time() - $timestamp) > 300) {
            return response()->json(['error' => 'Request expired'], 401);
        }
        // 验证Nonce(Redis实现)
        if (Redis::exists("nonce:{$nonce}")) {
            return response()->json(['error' => 'Nonce repeated'], 401);
        }
        // 重新生成签名
        $data = $request->all();
        $data['timestamp'] = $timestamp;
        $data['nonce'] = $nonce;
        ksort($data);
        $str = http_build_query($data);
        $expectedSignature = hash_hmac('sha256', $str, $appSecret);
        if (!hash_equals($expectedSignature, $signature)) {
            return response()->json(['error' => 'Invalid signature'], 401);
        }
        // 保存Nonce
        Redis::setex("nonce:{$nonce}", 300, time());
        return $next($request);
    }
    private function getSecretByKey($apiKey)
    {
        // 从数据库或配置中获取密钥
        return 'your_app_secret';
    }
}

安全建议

签名算法选择

  • 简单应用:MD5(但不够安全)
  • 生产环境:HMAC-SHA256
  • 高安全性:RSA非对称加密

防攻击措施

  • 时间限制:签名有有效期(建议5-10分钟)
  • 防重放:使用Nonce,一次请求只能使用一次
  • 请求限制:IP限流、用户限流

密钥管理

  • 定期更换密钥
  • 密钥分级管理(不同级别使用不同密钥)
  • 密钥加密存储

日志记录

  • 记录所有签名验证失败请求
  • 记录异常行为用于安全分析

完整集成示例

<?php
/**
 * API请求类(客户端)
 */
class ApiClient {
    private $baseUrl;
    private $appKey;
    private $appSecret;
    public function __construct($baseUrl, $appKey, $appSecret) {
        $this->baseUrl = $baseUrl;
        $this->appKey = $appKey;
        $this->appSecret = $appSecret;
    }
    public function request($method, $path, $params = []) {
        $timestamp = time();
        $nonce = bin2hex(random_bytes(16));
        // 构造签名数据
        $signData = array_merge([
            'app_key' => $this->appKey,
            'timestamp' => $timestamp,
            'nonce' => $nonce,
            'method' => strtoupper($method),
            'path' => $path
        ], $params);
        // 生成签名
        ksort($signData);
        $str = urldecode(http_build_query($signData));
        $sign = hash_hmac('sha256', $str, $this->appSecret);
        // 发送请求
        $headers = [
            'X-Api-Key: ' . $this->appKey,
            'X-Timestamp: ' . $timestamp,
            'X-Nonce: ' . $nonce,
            'X-Signature: ' . $sign,
            'Content-Type: application/json'
        ];
        // 使用curl发送请求
        $ch = curl_init($this->baseUrl . $path);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        if (strtoupper($method) === 'POST') {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
        }
        $response = curl_exec($ch);
        curl_close($ch);
        return json_decode($response, true);
    }
}

一个完整的API签名验证流程通常包含:

  1. 客户端负责生成签名(参数排序、拼接、加密)
  2. 服务端验证签名(检查时间戳、Nonce、重新生成签名比对)
  3. 安全防护(防重放攻击、防篡改、密钥管理等)

选择合适的签名算法和实现方案能显著提升API的安全性。

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