主题
OpenAPI 开放接口
OpenAPI
模块为平台提供了强大的接口开放能力。当您需要将后台接口暴露给外部系统或第三方开发者使用,但又不希望为每个接入方创建独立用户账号时,这个模块可以完美地解决这一需求。
核心优势
- 🔐 安全的签名验证机制
- 🚀 灵活的 QPS 限流控制
- 📊 完整的请求日志记录
- 💰 支持余额扣费模式
主要特性
- 签名验证:基于 HMAC-SHA256 的安全签名机制,确保接口调用的安全性
- 访问控制:支持按用户设置 QPS 限制,有效控制接口访问频率
- 日志记录:详细记录每次接口调用,便于监控和问题排查
- 余额管理:支持预付费模式,可按调用次数或其他维度扣费
数据库结构
OpenAPI 模块使用以下三个核心数据表来管理用户信息、余额和请求日志:
表名 | 描述 | 主要用途 |
---|---|---|
openapi_users | OpenAPI 用户表 | 存储第三方用户的基本信息和认证密钥 |
openapi_user_balance | 用户余额表 | 管理用户的可用余额和扣费记录 |
openapi_request_log | 请求日志表 | 记录所有 API 调用的详细信息 |
php
Schema::create('openapi_users', function (Blueprint $table) {
$table->id()->comment('ID');
$table->string('username')->comment('用户名');
$table->string('mobile', 20)->comment('手机号');
$table->string('password')->comment('密码');
$table->string('company')->nullable()->comment('公司名称');
$table->string('description')->nullable()->comment('描述');
$table->unsignedInteger('qps')->default(100)->comment('每分钟的 QPS');
$table->string('app_key')->comment('app key');
$table->string('app_secret')->comment('密钥');
$table->creatorId();
$table->createdAt();
$table->updatedAt();
$table->deletedAt();
$table->engine = 'InnoDB';
$table->comment('openapi 用户表');
});
Schema::create('openapi_user_balance', function (Blueprint $table) {
$table->id();
$table->uuid();
$table->integer('user_id')->comment('用户id');
$table->unsignedInteger('balance')->default(0)->comment('用户余额');
$table->createdAt();
$table->updatedAt();
$table->deletedAt();
$table->engine = 'InnoDB';
$table->comment('用户余额表');
});
Schema::create('openapi_request_log', function (Blueprint $table) {
$table->id();
$table->uuid('request_id')->comment('请求id');
$table->string('app_key')->comment('app key');
$table->json('data')->comment('请求数据');
$table->createdAt();
$table->updatedAt();
$table->engine = 'InnoDB';
$table->comment('openapi 请求日志');
});
创建 OpenAPI 用户
通过管理后台创建
在管理后台中找到 OpenAPI 模块,通过可视化界面创建新的第三方用户:
关键配置说明
QPS 限制说明
QPS
字段用于限制访问接口的频率,当前版本按每分钟计算。例如设置为 100,表示该用户每分钟最多可调用 100 次接口。
认证密钥
用户创建成功后,系统会自动分配一对认证密钥:
- AppKey:公开的应用标识符,用于识别调用方
- AppSecret:私密的签名密钥,用于生成请求签名,请妥善保管
安全提醒
AppSecret
仅在创建时显示一次,请及时记录保存- 建议定期更换密钥以提高安全性
- 切勿在客户端代码中硬编码
AppSecret
快速接入
路由配置
使用 OpenAPI 功能非常简单,只需在路由中添加相应的中间件即可。在项目根目录的 routes/api.php
文件中添加以下代码:
php
Route::prefix('v1')->middleware([
\Modules\Openapi\Middlewares\CheckSignatureMiddleware::class,
\Modules\Openapi\Middlewares\RateLimiterMiddleware::class // 可选:添加限流中间件
])->group(function () {
Route::get('user', function (){
return \Modules\Openapi\Facade\OpenapiResponse::success([
'message' => 'Hello OpenAPI!',
'timestamp' => time()
]);
});
// 更多 API 路由...
});
验证接入
配置完成后,在浏览器中访问 域名/api/v1/user
,如果看到以下响应,说明接入成功:
中间件说明
CheckSignatureMiddleware
:必需,用于验证请求签名RateLimiterMiddleware
:可选,用于限制请求频率
响应格式
所有 OpenAPI 接口都应使用统一的响应格式:
php
// 成功响应
return \Modules\Openapi\Facade\OpenapiResponse::success($data, $message);
// 错误响应
return \Modules\Openapi\Facade\OpenapiResponse::error($message, $code);
接口调用示例
签名算法说明
OpenAPI 使用 HMAC-SHA256 算法对请求参数进行签名验证,确保接口调用的安全性。签名流程如下:
- 收集参数:获取所有请求参数(包括 GET 查询参数或 POST 表单参数)
- 添加时间戳:添加
timestamp
参数(Unix 时间戳) - 数组扁平化:对嵌套数组和对象进行扁平化处理
- 对象:
user.name
->user.name
- 数组:
items[0].name
->items[0].name
- 对象:
- 参数排序:按参数名进行字典序排序
- 构建签名串:将参数按
key=value&key=value
格式拼接 - 生成签名:使用
AppSecret
对签名串进行 HMAC-SHA256 加密 - 发送请求:在请求头中添加
app-key
和signature
重要提醒
参数扁平化是签名验证的关键步骤,必须与服务端逻辑保持完全一致,否则会导致签名验证失败。
扁平化示例
假设有以下复杂参数:
json
{
"user": {
"name": "张三",
"age": 25
},
"items": [
{ "id": 1, "name": "商品A" },
{ "id": 2, "name": "商品B" }
],
"timestamp": 1640995200
}
扁平化后的参数:
json
{
"items[0].id": "1",
"items[0].name": "商品A",
"items[1].id": "2",
"items[1].name": "商品B",
"timestamp": "1640995200",
"user.age": "25",
"user.name": "张三"
}
最终签名字符串:
items[0].id=1&items[0].name=商品A&items[1].id=2&items[1].name=商品B×tamp=1640995200&user.age=25&user.name=张三
多语言调用示例
以下提供了多种编程语言的调用示例,您可以根据项目需求选择合适的实现方式:
javascript
const crypto = require('crypto')
const axios = require('axios')
class OpenAPIClient {
constructor(appKey, appSecret, baseURL) {
this.appKey = appKey
this.appSecret = appSecret
this.baseURL = baseURL
}
// 扁平化数组
flattenArray(obj, prefix = '') {
let result = {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key]
const newKey = prefix ? `${prefix}.${key}` : key
if (Array.isArray(value)) {
// 处理数组
value.forEach((item, index) => {
if (typeof item === 'object' && item !== null) {
Object.assign(result, this.flattenArray(item, `${newKey}[${index}]`))
} else {
result[`${newKey}[${index}]`] = item
}
})
} else if (typeof value === 'object' && value !== null) {
// 处理对象
Object.assign(result, this.flattenArray(value, newKey))
} else {
result[newKey] = value
}
}
}
return result
}
// 创建签名
createSignature(params) {
// 扁平化参数
const flattenedParams = this.flattenArray(params)
// 按键名排序
const keys = Object.keys(flattenedParams).sort()
// 构建签名字符串
const signStr = keys.map((key) => `${key}=${flattenedParams[key]}`).join('&')
return crypto.createHmac('sha256', this.appSecret).update(signStr).digest('hex')
}
// GET 请求
async get(url, params = {}) {
params.timestamp = Math.floor(Date.now() / 1000)
const signature = this.createSignature(params)
const response = await axios.get(`${this.baseURL}${url}`, {
params,
headers: {
'app-key': this.appKey,
signature: signature
}
})
return response.data
}
// POST 请求
async post(url, data = {}) {
data.timestamp = Math.floor(Date.now() / 1000)
const signature = this.createSignature(data)
const response = await axios.post(`${this.baseURL}${url}`, data, {
headers: {
'app-key': this.appKey,
signature: signature,
'Content-Type': 'application/x-www-form-urlencoded'
}
})
return response.data
}
}
// 使用示例
const client = new OpenAPIClient('your_app_key', 'your_app_secret', 'https://your-domain.com/api')
// GET 请求示例
client
.get('/v1/user', { page: 1, limit: 10 })
.then((result) => console.log(result))
.catch((error) => console.error(error))
// POST 请求示例
client
.post('/v1/user', { name: 'John', email: 'john@example.com' })
.then((result) => console.log(result))
.catch((error) => console.error(error))
// 复杂参数示例
client
.post('/v1/user', {
user: { name: '张三', age: 25 },
items: [
{ id: 1, name: '商品A' },
{ id: 2, name: '商品B' }
]
})
.then((result) => console.log(result))
.catch((error) => console.error(error))
php
<?php
class OpenAPIClient
{
private $appKey;
private $appSecret;
private $baseURL;
public function __construct($appKey, $appSecret, $baseURL)
{
$this->appKey = $appKey;
$this->appSecret = $appSecret;
$this->baseURL = rtrim($baseURL, '/');
}
/**
* 扁平化数组(与服务端逻辑保持一致)
*/
private function flattenArray($array, $prefix = '')
{
$result = [];
foreach ($array as $key => $value) {
$newKey = $prefix ? $prefix . '.' . $key : $key;
if (is_array($value)) {
if ($this->isAssociativeArray($value)) {
// 关联数组
$result = array_merge($result, $this->flattenArray($value, $newKey));
} else {
// 索引数组
foreach ($value as $index => $item) {
if (is_array($item)) {
$result = array_merge($result, $this->flattenArray($item, $newKey . '[' . $index . ']'));
} else {
$result[$newKey . '[' . $index . ']'] = $item;
}
}
}
} else {
$result[$newKey] = $value;
}
}
return $result;
}
/**
* 判断是否为关联数组
*/
private function isAssociativeArray($array)
{
if (empty($array)) {
return false;
}
return array_keys($array) !== range(0, count($array) - 1);
}
/**
* 创建签名(与服务端逻辑完全一致)
*/
private function createSignature($params)
{
// 扁平化参数
$flattenedParams = $this->flattenArray($params);
// 按键名排序
ksort($flattenedParams);
// 构建签名字符串
$signStr = '';
foreach ($flattenedParams as $key => $value) {
$signStr .= $key . '=' . $value . '&';
}
// 去除末尾的 & 符号
$signStr = rtrim($signStr, '&');
return hash_hmac('sha256', $signStr, $this->appSecret);
}
/**
* GET 请求
*/
public function get($url, $params = [])
{
$params['timestamp'] = time();
$signature = $this->createSignature($params);
$queryString = http_build_query($params);
$fullUrl = $this->baseURL . $url . '?' . $queryString;
$headers = [
'app-key: ' . $this->appKey,
'signature: ' . $signature
];
return $this->makeRequest($fullUrl, 'GET', $headers);
}
/**
* POST 请求
*/
public function post($url, $data = [])
{
$data['timestamp'] = time();
$signature = $this->createSignature($data);
$headers = [
'app-key: ' . $this->appKey,
'signature: ' . $signature,
'Content-Type: application/x-www-form-urlencoded'
];
$fullUrl = $this->baseURL . $url;
return $this->makeRequest($fullUrl, 'POST', $headers, http_build_query($data));
}
/**
* 执行 HTTP 请求
*/
private function makeRequest($url, $method, $headers, $data = null)
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 30,
]);
if ($data && $method === 'POST') {
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new Exception("HTTP Error: " . $httpCode);
}
return json_decode($response, true);
}
}
// 使用示例
$client = new OpenAPIClient('your_app_key', 'your_app_secret', 'https://your-domain.com/api');
try {
// GET 请求示例
$result = $client->get('/v1/user', ['page' => 1, 'limit' => 10]);
echo "GET Result: " . json_encode($result) . "\n";
// POST 请求示例
$result = $client->post('/v1/user', ['name' => 'John', 'email' => 'john@example.com']);
echo "POST Result: " . json_encode($result) . "\n";
// 复杂参数示例
$complexParams = [
'user' => [
'name' => '张三',
'age' => 25
],
'items' => [
['id' => 1, 'name' => '商品A'],
['id' => 2, 'name' => '商品B']
]
];
$result = $client->post('/v1/user', $complexParams);
echo "Complex Result: " . json_encode($result) . "\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
python
import hashlib
import hmac
import time
import requests
from urllib.parse import urlencode
from typing import Dict, Any, Optional
class OpenAPIClient:
def __init__(self, app_key: str, app_secret: str, base_url: str):
self.app_key = app_key
self.app_secret = app_secret
self.base_url = base_url.rstrip('/')
def _flatten_array(self, obj: Dict[str, Any], prefix: str = '') -> Dict[str, Any]:
"""扁平化数组(与服务端逻辑保持一致)"""
result = {}
for key, value in obj.items():
new_key = f"{prefix}.{key}" if prefix else key
if isinstance(value, list):
# 处理数组
for index, item in enumerate(value):
if isinstance(item, dict):
result.update(self._flatten_array(item, f"{new_key}[{index}]"))
else:
result[f"{new_key}[{index}]"] = item
elif isinstance(value, dict):
# 处理对象
result.update(self._flatten_array(value, new_key))
else:
result[new_key] = value
return result
def _create_signature(self, params: Dict[str, Any]) -> str:
"""创建签名(与服务端逻辑完全一致)"""
# 扁平化参数
flattened_params = self._flatten_array(params)
# 按键名排序
sorted_params = sorted(flattened_params.items())
# 构建签名字符串
sign_str = '&'.join([f"{key}={value}" for key, value in sorted_params])
# 使用 HMAC-SHA256 生成签名
signature = hmac.new(
self.app_secret.encode('utf-8'),
sign_str.encode('utf-8'),
hashlib.sha256
).hexdigest()
return signature
def get(self, url: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""GET 请求"""
if params is None:
params = {}
# 添加时间戳
params['timestamp'] = int(time.time())
# 生成签名
signature = self._create_signature(params)
# 构建请求头
headers = {
'app-key': self.app_key,
'signature': signature
}
# 发送请求
full_url = f"{self.base_url}{url}"
response = requests.get(full_url, params=params, headers=headers, timeout=30)
response.raise_for_status()
return response.json()
def post(self, url: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""POST 请求"""
if data is None:
data = {}
# 添加时间戳
data['timestamp'] = int(time.time())
# 生成签名
signature = self._create_signature(data)
# 构建请求头
headers = {
'app-key': self.app_key,
'signature': signature,
'Content-Type': 'application/x-www-form-urlencoded'
}
# 发送请求
full_url = f"{self.base_url}{url}"
response = requests.post(full_url, data=data, headers=headers, timeout=30)
response.raise_for_status()
return response.json()
# 使用示例
if __name__ == "__main__":
client = OpenAPIClient(
app_key='your_app_key',
app_secret='your_app_secret',
base_url='https://your-domain.com/api'
)
try:
# GET 请求示例
result = client.get('/v1/user', {'page': 1, 'limit': 10})
print(f"GET Result: {result}")
# POST 请求示例
result = client.post('/v1/user', {'name': 'John', 'email': 'john@example.com'})
print(f"POST Result: {result}")
except requests.exceptions.RequestException as e:
print(f"Request Error: {e}")
except Exception as e:
print(f"Error: {e}")
bash
#!/bin/bash
# 配置参数
APP_KEY="your_app_key"
APP_SECRET="your_app_secret"
BASE_URL="https://your-domain.com/api"
# 创建签名函数(与服务端逻辑保持一致)
create_signature() {
local params="$1"
# 注意:cURL 脚本中的参数需要手动扁平化和排序
# 对于复杂参数,建议使用其他语言的客户端
echo -n "$params" | openssl dgst -sha256 -hmac "$APP_SECRET" -hex | sed 's/^.* //'
}
# GET 请求示例
echo "=== GET 请求示例 ==="
TIMESTAMP=$(date +%s)
PARAMS="page=1&limit=10×tamp=$TIMESTAMP"
SIGNATURE=$(create_signature "$PARAMS")
curl -X GET \
"$BASE_URL/v1/user?$PARAMS" \
-H "app-key: $APP_KEY" \
-H "signature: $SIGNATURE" \
-H "Content-Type: application/json"
echo -e "\n"
# POST 请求示例
echo "=== POST 请求示例 ==="
TIMESTAMP=$(date +%s)
POST_DATA="name=John&email=john@example.com×tamp=$TIMESTAMP"
SIGNATURE=$(create_signature "$POST_DATA")
curl -X POST \
"$BASE_URL/v1/user" \
-H "app-key: $APP_KEY" \
-H "signature: $SIGNATURE" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "$POST_DATA"
echo -e "\n"
# 使用 jq 格式化 JSON 响应(需要安装 jq)
echo "=== 带格式化输出的请求 ==="
TIMESTAMP=$(date +%s)
PARAMS="timestamp=$TIMESTAMP"
SIGNATURE=$(create_signature "$PARAMS")
curl -s -X GET \
"$BASE_URL/v1/user?$PARAMS" \
-H "app-key: $APP_KEY" \
-H "signature: $SIGNATURE" \
| jq '.'
ApiFox 前置脚本
如果您使用 ApiFox 进行接口测试,可以添加以下前置脚本来自动处理签名:
javascript
// 在 ApiFox 环境变量中配置 app_key 和 app_secret
const appKey = pm.environment.get('app_key');
const appSecret = pm.environment.get('app_secret');
// 扁平化数组函数(与服务端逻辑保持一致)
function flattenArray(obj, prefix = '') {
let result = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
const newKey = prefix ? `${prefix}.${key}` : key;
if (Array.isArray(value)) {
// 处理数组
value.forEach((item, index) => {
if (typeof item === 'object' && item !== null) {
Object.assign(result, flattenArray(item, `${newKey}[${index}]`));
} else {
result[`${newKey}[${index}]`] = item;
}
});
} else if (typeof value === 'object' && value !== null) {
// 处理对象
Object.assign(result, flattenArray(value, newKey));
} else {
result[newKey] = value;
}
}
}
return result;
}
function createSign(params) {
// 扁平化参数
const flattenedParams = flattenArray(params);
// 按键名排序
const keys = Object.keys(flattenedParams).sort();
// 构建签名字符串
const signStr = keys.map((key) => `${key}=${flattenedParams[key]}`).join('&');
return CryptoJS.HmacSHA256(signStr, appSecret).toString();
}
let params = {};
params['timestamp'] = Math.floor(Date.now() / 1000);
if (pm.request.method == 'GET') {
// 处理 GET 请求参数
const queryString = pm.request.url.getQueryString();
if (queryString) {
pm.request.url
.getQueryString()
.split('&')
.forEach((item) => {
const items = item.split('=');
params[items[0]] = items[1];
});
}
const signature = createSign(params);
// 设置请求头
pm.request.headers.add({ key: 'app-key', value: appKey });
pm.request.headers.add({ key: 'signature', value: signature });
// 更新查询参数
let query = '';
for (let key in params) {
query += key + '=' + params[key] + '&';
}
pm.request.url.query = query.slice(0, -1); // 去除末尾的 &
} else if (pm.request.method == 'POST') {
// 处理 POST 请求参数
if (pm.request.body.mode == 'urlencoded') {
pm.request.body.urlencoded.each((item) => {
params[item.key] = item.value;
});
const signature = createSign(params);
// 设置请求头
pm.request.headers.add({ key: 'app-key', value: appKey });
pm.request.headers.add({ key: 'signature', value: signature });
// 添加时间戳参数
pm.request.body.urlencoded.add({
disabled: false,
key: 'timestamp',
value: params.timestamp
});
}
// 支持 form-data 格式
if (pm.request.body.mode == 'formdata') {
pm.request.body.formdata.each((item) => {
params[item.key] = item.value;
});
const signature = createSign(params);
pm.request.headers.add({ key: 'app-key', value: appKey });
pm.request.headers.add({ key: 'signature', value: signature });
pm.request.body.formdata.add({
key: 'timestamp',
value: params.timestamp
});
}
}
环境配置
在 ApiFox 中配置环境变量,设置您的 app_key
和 app_secret
:
测试验证
配置完成后,通过 ApiFox 发送请求,如果出现以下结果,说明接入成功:
测试建议
- 先使用简单的 GET 请求进行测试
- 确认时间戳生成和签名算法正确
- 检查请求头是否正确设置
- 验证参数排序逻辑
中间件配置
OpenAPI 模块提供了两个核心中间件来确保接口的安全性和稳定性:
可用中间件
中间件 | 类名 | 功能描述 | 是否必需 |
---|---|---|---|
签名验证 | CheckSignatureMiddleware | 验证请求签名的有效性 | ✅ 必需 |
速率限制 | RateLimiterMiddleware | 控制用户请求频率(QPS) | 🔄 可选 |
使用方式
php
Route::prefix('v1')->middleware([
\Modules\Openapi\Middlewares\CheckSignatureMiddleware::class, // 必需
\Modules\Openapi\Middlewares\RateLimiterMiddleware::class // 可选
])->group(function () {
// 您的 API 路由
});
注意事项
CheckSignatureMiddleware
是必需的,用于保证接口安全RateLimiterMiddleware
建议在生产环境中使用,防止接口滥用- 中间件的顺序很重要:签名验证应该在速率限制之前
异常处理
自定义异常
OpenAPI 模块的所有异常都需要继承 OpenapiException
基类。这样可以确保异常处理的一致性:
php
namespace Modules\Openapi\Exceptions;
use Modules\Openapi\Enums\Code;
// 示例:不合法的 AppKey 异常
class InvalidAppKeyException extends OpenapiException
{
protected $code = Code::INVALID_APP_KEY;
public function __construct($message = null)
{
parent::__construct($message ?: '无效的 AppKey');
}
}
异常渲染配置
在 bootstrap/app.php
文件中配置 OpenAPI 异常的统一渲染处理:
php
$app = Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
// 中间件配置
})
->withExceptions(function (Exceptions $exceptions) {
// OpenAPI 异常统一处理
$exceptions->render(function (OpenapiException $exception, Request $request) {
return OpenapiResponse::error(
$exception->getMessage(),
$exception->getCode()
);
});
// 其他异常处理...
})->create();
异常处理说明
- 所有 OpenAPI 相关异常都会被自动捕获并格式化返回
- 异常响应格式与成功响应保持一致
- 建议为不同的错误场景创建专门的异常类
状态码枚举
枚举定义
OpenAPI 模块使用统一的状态码枚举来标识不同的响应状态。所有枚举都需要实现 Modules\Openapi\Enums\Enum
接口:
php
namespace Modules\Openapi\Enums;
enum Code: int implements Enum
{
// 基础状态码
case SUCCESS = 10000; // 请求成功
case FAILED = 10001; // 请求失败
// 认证相关错误
case APP_KEY_LOST = 10002; // AppKey 丢失
case SIGNATURE_LOST = 10003; // 签名丢失
case INVALID_APP_KEY = 10004; // 无效的 AppKey
case INVALID_SIGNATURE = 10005; // 无效的签名
case INVALID_TIMESTAMP = 10006; // 无效的时间戳
// 业务相关错误
case BALANCE_NOT_ENOUGH = 10007; // 余额不足
case RATE_LIMIT = 10008; // 请求频率超限
/**
* 获取状态码对应的中文描述
*/
public function name(): string
{
return match ($this) {
self::SUCCESS => '请求成功',
self::FAILED => '请求失败',
self::APP_KEY_LOST => 'AppKey 丢失',
self::SIGNATURE_LOST => '签名丢失',
self::INVALID_APP_KEY => '无效的 AppKey',
self::INVALID_SIGNATURE => '无效的签名',
self::INVALID_TIMESTAMP => '无效的时间戳',
self::BALANCE_NOT_ENOUGH => '余额不足',
self::RATE_LIMIT => '请求过于频繁,请稍后重试'
};
}
/**
* 比较枚举值
* @param mixed $value
* @return bool
*/
public function equal(mixed $value): bool
{
return $this->value === $value;
}
}
状态码说明
状态码 | 常量名 | 描述 | 触发场景 |
---|---|---|---|
10000 | SUCCESS | 请求成功 | 正常响应 |
10001 | FAILED | 请求失败 | 通用错误 |
10002 | APP_KEY_LOST | AppKey 丢失 | 请求头缺少 app-key |
10003 | SIGNATURE_LOST | 签名丢失 | 请求头缺少 signature |
10004 | INVALID_APP_KEY | 无效的 AppKey | AppKey 不存在或已禁用 |
10005 | INVALID_SIGNATURE | 无效的签名 | 签名验证失败 |
10006 | INVALID_TIMESTAMP | 无效的时间戳 | 时间戳过期或格式错误 |
10007 | BALANCE_NOT_ENOUGH | 余额不足 | 用户余额不够扣费 |
10008 | RATE_LIMIT | 请求频率超限 | 超过 QPS 限制 |
自定义状态码
如需添加新的状态码,建议从 10100 开始,避免与系统预留码冲突。
最佳实践
安全建议
密钥管理
- 定期更换
AppSecret
,建议每 3-6 个月更换一次 - 使用环境变量存储密钥,避免硬编码
- 为不同环境(开发、测试、生产)使用不同的密钥
- 定期更换
签名验证
- 时间戳有效期建议设置为 5-15 分钟
- 对敏感接口可以添加额外的业务级验证
- 记录异常签名尝试,便于安全审计
频率控制
- 根据业务场景合理设置 QPS 限制
- 考虑为不同类型的接口设置不同的限流策略
- 提供友好的限流提示信息
性能优化
缓存策略
- 缓存用户的
AppKey
和AppSecret
映射关系 - 使用 Redis 等缓存工具提高验证效率
- 合理设置缓存过期时间
- 缓存用户的
日志管理
- 定期清理过期的请求日志
- 考虑使用异步方式记录日志
- 重要信息可以同步到专门的日志系统
监控告警
建议监控以下指标:
- API 调用成功率
- 平均响应时间
- 异常签名尝试次数
- QPS 超限频率
- 用户余额预警
常见问题
签名验证失败
问题:接口返回 "无效的签名" 错误
解决方案:
- 检查参数排序是否正确(按字典序)
- 确认时间戳格式(Unix 时间戳)
- 验证
AppSecret
是否正确 - 确保参数拼接格式为
key=value&key=value
- 重要:检查数组扁平化是否正确实现(嵌套对象和数组必须扁平化)
请求频率超限
问题:接口返回 "请求过于频繁" 错误
解决方案:
- 检查当前 QPS 设置
- 优化调用频率,添加请求间隔
- 联系管理员调整 QPS 限制
- 考虑使用批量接口减少调用次数
时间戳过期
问题:接口返回 "无效的时间戳" 错误
解决方案:
- 确保服务器时间同步
- 检查时间戳生成逻辑(必须是 Unix 时间戳)
- 默认时间窗口为 60 秒,确保请求在有效时间内发送
- 使用 NTP 服务同步时间
时间戳验证逻辑
服务端验证:abs(time() - $timestamp) <= 60
(默认 60 秒) 可通过配置 api.timestamp_period
调整时间窗口
技术支持
如果在使用过程中遇到问题,请参考本文档或联系技术支持团队。