| 原則 | 說明 | 範例 |
|---|---|---|
| 資源導向 | URL 表達資源名詞,動詞由 HTTP Method 表示 | GET /api/v1/users/123 |
| 版本控制 | URL 包含版本號,便於迭代兼容 | /api/v1/、/api/v2/ |
| 統一響應格式 | 固定結構,前端統一處理 | {code, message, data, meta} |
| 狀態碼語義化 | HTTP Status Code 表達結果類別 | 200/201/400/401/403/404/422/500 |
| 冪等性 | GET/PUT/DELETE 多次調用結果一致 | POST 非冪等 |
<?php
// 成功響應
[
'code' => 200,
'message' => 'success',
'data' => [...], // 業務數據
'meta' => [ // 分頁/額外元數據
'page' => 1,
'per_page' => 15,
'total' => 100
]
]
// 失敗響應
[
'code' => 422,
'message' => '驗證失敗',
'errors' => [
'email' => ['郵箱格式不正確'],
'password' => ['密碼長度至少 8 位']
]
]
<?php
namespace app\api\controller;
use think\facade\Request;
use think\facade\Validate;
use think\response\Json;
class BaseController
{
protected function success($data = [], $message = 'success', $meta = []): Json
{
return json([
'code' => 200,
'message' => $message,
'data' => $data,
'meta' => $meta,
]);
}
protected function error($message = 'error', $code = 400, $errors = []): Json
{
return json([
'code' => $code,
'message' => $message,
'errors' => $errors,
], $code);
}
protected function validate(array $rules, array $data = []): array
{
$validate = Validate::rule($rules);
if (!$validate->check($data ?: Request::param())) {
throw new \think\exception\ValidateException($validate->getError());
}
return $data ?: Request::param();
}
}
<?php
namespace app\api\controller\v1;
use app\api\controller\BaseController;
use app\model\User;
use think\facade\Db;
class Auth extends BaseController
{
public function register()
{
// 1. 參數驗證
$data = $this->validate([
'username' => 'require|alphaNum|length:3,20|unique:user',
'email' => 'require|email|unique:user',
'password' => 'require|length:8,32|confirmed', // 需傳 password_confirm
'mobile' => 'require|regex:/^1[3-9]\d{9}$/|unique:user',
]);
// 2. 業務處理 (事務)
try {
$user = Db::transaction(function () use ($data) {
$user = User::create([
'username' => $data['username'],
'email' => $data['email'],
'password' => password_hash($data['password'], PASSWORD_DEFAULT),
'mobile' => $data['mobile'],
'status' => 1,
]);
// 可擴展:發送歡迎郵件、初始化配置、記錄註冊日誌
return $user;
});
return $this->success($user->hidden(['password'])->toArray(), '註冊成功');
} catch (\Throwable $e) {
return $this->error('註冊失敗:' . $e->getMessage(), 500);
}
}
}
防篡改、防重放、身份驗證三合一:
<?php
function generateSign(array $params, string $secret, int $timestamp): string
{
// 1. 過濾空值、sign 字段
$params = array_filter($params, fn($v) => $v !== '' && $v !== null);
unset($params['sign']);
// 2. 字典序排序
ksort($params);
// 3. 拼裝字串: key1=value1&key2=value2...
$string = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
// 4. 加入時間戳防重放 (5分鐘有效期)
$string .= "×tamp=$timestamp";
// 5. HMAC-SHA256
return hash_hmac('sha256', $string, $secret);
}
// 驗證中間件
public function handle($request, \Closure $next)
{
$timestamp = (int)$request->header('X-Timestamp');
$sign = $request->header('X-Sign');
$appId = $request->header('X-App-Id');
// 時間戳檢查 (防重放)
if (abs(time() - $timestamp) > 300) {
return json(['code' => 401, 'message' => '請求已過期'], 401);
}
// 查詢 AppSecret
$app = AppModel::where('app_id', $appId)->find();
if (!$app) {
return json(['code' => 401, 'message' => '無效的 AppID'], 401);
}
// 重新計算簽名比對
$params = $request->param();
$expectedSign = generateSign($params, $app->app_secret, $timestamp);
if (!hash_equals($expectedSign, $sign)) {
return json(['code' => 401, 'message' => '簽名驗證失敗'], 401);
}
// 防重放:Redis 存儲已用簽名 (key: sign:{sign}, ttl: 300s)
if (Redis::exists("sign:$sign")) {
return json(['code' => 401, 'message' => '重複請求'], 401);
}
Redis::setex("sign:$sign", 300, 1);
return $next($request);
}
// route/api.php
Route::group('v1', function () {
Route::post('auth/register', 'v1.Auth/register');
Route::post('auth/login', 'v1.Auth/login');
Route::middleware('auth:api')->group(function () {
Route::get('user/profile', 'v1.User/profile');
Route::put('user/profile', 'v1.User/updateProfile');
Route::apiResource('posts', 'v1.Post'); // 批量生成 index/create/store/show/update/destroy
});
})->prefix('api');
swagger-php 註解生成 swagger.json,配合 Swagger UI