本文目录导读:

在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:国内金融级要求
建议根据你的具体业务场景选择适合的安全级别,如果是一般的应用,使用第一种方案就够了;如果涉及资金交易,建议使用第二种方案。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播