PHP项目如何实现用户自定义验签?

wen java案例 4

本文目录导读:

PHP项目如何实现用户自定义验签?

  1. 核心设计思路
  2. 完整实现代码
  3. 数据库设计(用户自定义配置)
  4. 安全建议
  5. 使用示例

在PHP项目中实现用户自定义验签,通常用于API接口安全、支付回调验证、数据完整性校验等场景,下面我给你一个完整的、可复用的实现方案。

核心设计思路

  1. 签名算法:用户可选择的签名方式(如MD5、SHA256、HMAC等)
  2. 参数排序:按字母顺序排序参数,保持签名的确定性
  3. 密钥管理:用户自定义密钥
  4. 防重放攻击:加入时间戳和随机数(Nonce)

完整实现代码

签名工具类

<?php
namespace App\Utils;
class SignUtil
{
    /**
     * 生成签名
     *
     * @param array $params 待签名的参数
     * @param string $secretKey 密钥
     * @param string $method 签名算法 (md5, sha256, hmac-md5, hmac-sha256)
     * @param bool $addTimestamp 是否自动添加时间戳
     * @param bool $addNonce 是否自动添加随机数
     * @return string
     */
    public static function generateSign(
        array $params,
        string $secretKey,
        string $method = 'md5',
        bool $addTimestamp = true,
        bool $addNonce = true
    ): string {
        // 1. 过滤掉签名字段本身和空值
        $params = self::filterParams($params);
        // 2. 添加时间戳和随机数(如果需要)
        if ($addTimestamp) {
            $params['timestamp'] = time();
        }
        if ($addNonce) {
            $params['nonce'] = self::generateNonce();
        }
        // 3. 按键名字母升序排序
        ksort($params);
        // 4. 拼接参数
        $paramStr = self::buildQueryString($params);
        // 5. 根据算法生成签名
        return self::signByMethod($paramStr, $secretKey, $method);
    }
    /**
     * 验证签名
     *
     * @param array $params 包含签名的所有参数
     * @param string $secretKey 密钥
     * @param string $method 签名算法
     * @param int $timeout 时间戳有效时长(秒),0表示不验证
     * @return bool
     */
    public static function verifySign(
        array $params,
        string $secretKey,
        string $method = 'md5',
        int $timeout = 300
    ): bool {
        // 1. 检查签名是否存在
        if (!isset($params['sign'])) {
            return false;
        }
        $clientSign = $params['sign'];
        // 2. 过滤掉签名字段
        $filteredParams = self::filterParams($params);
        // 3. 验证时间戳(防重放)
        if ($timeout > 0 && isset($filteredParams['timestamp'])) {
            $timestamp = intval($filteredParams['timestamp']);
            if (abs(time() - $timestamp) > $timeout) {
                return false; // 签名超时
            }
        }
        // 4. 直接使用当前参数重新生成签名(不考虑签名算法中的自动添加)
        // 这里不能调用 generateSign(),因为会重新添加 timestamp/nonce
        // 我们需要手动处理
        // 5. 按键名字母升序排序
        ksort($filteredParams);
        // 6. 拼接参数
        $paramStr = self::buildQueryString($filteredParams);
        // 7. 计算服务端签名
        $serverSign = self::signByMethod($paramStr, $secretKey, $method);
        // 8. 比较签名(建议使用安全比较)
        return hash_equals($clientSign, $serverSign);
    }
    /**
     * 过滤参数:移除sign字段和空值
     */
    private static function filterParams(array $params): array
    {
        $result = [];
        foreach ($params as $key => $value) {
            if ($key === 'sign' || $value === '' || $value === null) {
                continue;
            }
            $result[$key] = $value;
        }
        return $result;
    }
    /**
     * 拼接查询字符串(URL编码)
     */
    private static function buildQueryString(array $params): string
    {
        $parts = [];
        foreach ($params as $key => $value) {
            // 处理数组参数:a[0]=1&a[1]=2
            if (is_array($value)) {
                $value = json_encode($value, JSON_UNESCAPED_UNICODE);
            }
            $parts[] = $key . '=' . urlencode((string)$value);
        }
        return implode('&', $parts);
    }
    /**
     * 根据指定算法生成签名
     */
    private static function signByMethod(string $paramStr, string $secretKey, string $method): string
    {
        switch (strtolower($method)) {
            case 'sha256':
                return hash('sha256', $paramStr . $secretKey);
            case 'sha1':
                return sha1($paramStr . $secretKey);
            case 'hmac-md5':
                return hash_hmac('md5', $paramStr, $secretKey);
            case 'hmac-sha256':
                return hash_hmac('sha256', $paramStr, $secretKey);
            case 'md5':
            default:
                return md5($paramStr . $secretKey);
        }
    }
    /**
     * 生成随机数
     */
    private static function generateNonce(int $length = 16): string
    {
        return bin2hex(random_bytes($length / 2));
    }
}

用户配置管理(支持自定义验签)

<?php
namespace App\Models;
class UserSignConfig
{
    private $userId;
    private $secretKey;   // 用户自定义密钥
    private $signMethod;  // 用户自定义签名算法
    public function __construct(int $userId, string $secretKey, string $signMethod = 'md5')
    {
        $this->userId = $userId;
        $this->secretKey = $secretKey;
        $this->signMethod = $signMethod;
    }
    /**
     * 获取用户的签名配置(可从数据库读取)
     */
    public static function getByUserId(int $userId): ?self
    {
        // 这里应该从数据库查询,示例代码直接返回模拟数据
        // $user = DB::table('user_sign_configs')->where('user_id', $userId)->first();
        $dbData = [
            1 => ['secret_key' => 'abc123', 'sign_method' => 'sha256'],
            2 => ['secret_key' => 'xyz789', 'sign_method' => 'hmac-md5'],
        ];
        if (isset($dbData[$userId])) {
            return new self(
                $userId,
                $dbData[$userId]['secret_key'],
                $dbData[$userId]['sign_method']
            );
        }
        return null;
    }
    public function getSecretKey(): string
    {
        return $this->secretKey;
    }
    public function getSignMethod(): string
    {
        return $this->signMethod;
    }
}

中间件实现(自动验证签名)

<?php
namespace App\Http\Middleware;
use Closure;
use App\Utils\SignUtil;
use App\Models\UserSignConfig;
class VerifyUserSign
{
    public function handle($request, Closure $next)
    {
        $params = $request->all();
        // 获取用户ID(根据你的认证方式)
        $userId = $request->input('user_id') ?? $request->header('X-User-Id');
        if (!$userId) {
            return response()->json(['code' => 400, 'msg' => '缺少用户标识'], 400);
        }
        // 获取用户的签名配置
        $userConfig = UserSignConfig::getByUserId($userId);
        if (!$userConfig) {
            return response()->json(['code' => 400, 'msg' => '用户验签配置不存在'], 400);
        }
        // 验证签名
        $isValid = SignUtil::verifySign(
            $params,
            $userConfig->getSecretKey(),
            $userConfig->getSignMethod(),
            300 // 5分钟超时
        );
        if (!$isValid) {
            return response()->json(['code' => 401, 'msg' => '签名验证失败'], 401);
        }
        return $next($request);
    }
}

客户端调用示例(PHP)

<?php
// 客户端准备请求
$userId = 1;
$secretKey = 'abc123'; // 从用户配置获取
$signMethod = 'sha256';
// 业务参数
$params = [
    'user_id' => $userId,
    'name' => '张三',
    'amount' => 100.50,
    'order_id' => 'ORDER20231201',
];
// 生成签名
$sign = SignUtil::generateSign(
    $params,
    $secretKey,
    $signMethod,
    true,  // 自动加时间戳
    true   // 自动加随机数
);
// 将签名加入参数
$params['sign'] = $sign;
// 发送请求
$ch = curl_init('https://your-api.com/api/submit');
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

数据库设计(用户自定义配置)

CREATE TABLE `user_sign_configs` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `user_id` int(10) unsigned NOT NULL COMMENT '用户ID',
    `secret_key` varchar(64) NOT NULL COMMENT '用户自定义密钥',
    `sign_method` varchar(20) NOT NULL DEFAULT 'md5' COMMENT '签名算法(md5,sha256,hmac-md5,hmac-sha256)',
    `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1启用,0禁用',
    `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户签名配置表';

安全建议

  1. 密钥管理:密钥要在服务端加密存储,前端只下发一次
  2. 防重放攻击:一定要验证时间戳,建议超时时间300秒
  3. 签名比较:使用 hash_equals() 防止时序攻击
  4. 日志记录:记录签名验证失败的日志,方便排查问题
  5. 限流:配合接口限流,防止暴力破解签名

使用示例

// 1. 生成签名(客户端)
$sign = SignUtil::generateSign(
    ['a' => 1, 'b' => 2],
    'my_secret_key',
    'sha256'
);
// 2. 验证签名(服务端)
$result = SignUtil::verifySign(
    ['a' => 1, 'b' => 2, 'sign' => $sign, 'timestamp' => time()],
    'my_secret_key',
    'sha256'
);

这套方案灵活、安全、可扩展,用户可以根据自己的安全需求选择不同的签名算法和密钥。

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