本文目录导读:

在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. 返回结果
| |
注意事项
- 参数传递顺序:不要依赖参数的顺序,始终按Key排序
- 数组参数处理:如果包含嵌套数组,需要递归处理
- 特殊字符:URL编码时注意 + 和 空格 的处理
- 多语言兼容:不同语言的处理方式可能略有差异,建议写详细的签名规则文档
- 日志记录:建议记录签名验证失败的信息,便于排查问题
更高级的方案(开发量较大)
- OAuth 2.0:用于第三方授权
- JWT:无状态认证,适合分布式系统
- 国密SM2/SM3:国内金融级要求
建议根据你的具体业务场景选择适合的安全级别,如果是一般的应用,使用第一种方案就够了;如果涉及资金交易,建议使用第二种方案。