本文目录导读:

在PHP项目中实现用户自定义验签,通常用于API接口安全、支付回调验证、数据完整性校验等场景,下面我给你一个完整的、可复用的实现方案。
核心设计思路
- 签名算法:用户可选择的签名方式(如MD5、SHA256、HMAC等)
- 参数排序:按字母顺序排序参数,保持签名的确定性
- 密钥管理:用户自定义密钥
- 防重放攻击:加入时间戳和随机数(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='用户签名配置表';
安全建议
- 密钥管理:密钥要在服务端加密存储,前端只下发一次
- 防重放攻击:一定要验证时间戳,建议超时时间300秒
- 签名比较:使用
hash_equals()防止时序攻击 - 日志记录:记录签名验证失败的日志,方便排查问题
- 限流:配合接口限流,防止暴力破解签名
使用示例
// 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'
);
这套方案灵活、安全、可扩展,用户可以根据自己的安全需求选择不同的签名算法和密钥。