用 PHP 构建轻量级 REST API:从路由到鉴权的完整实践

1. 引言

在微服务与前后端分离日益普及的今天,REST API 已成为系统间通信的主流方式。PHP 凭借其部署简单、生态成熟的特点,依然是构建 Web 服务的常用选择。本文不引入 Laravel、Symfony 等重型框架,而是从零开始,用 PHP 原生能力配合轻量组件,搭建一个结构清晰、易于维护的 REST API 服务。

我们将依次覆盖 REST 设计原则、路由定义、请求解析、JSON 响应封装、状态码与错误处理,以及 API Key / Bearer Token 两种简单鉴权方式。整个项目不依赖 Composer 之外的第三方框架,代码可直接运行。

2. REST 设计原则简述

REST(Representational State Transfer)是一种基于 HTTP 协议的架构风格,核心思想是将一切视为资源,通过统一的接口对资源进行操作。理解以下原则,是设计好 API 的第一步。

2.1 资源与 URL 设计

资源是 REST 的核心抽象,URL 只用来定位资源,不包含动词。例如:

  • GET /api/users:获取用户列表
  • POST /api/users:创建用户
  • GET /api/users/42:获取单个用户
  • PUT /api/users/42:整体更新用户
  • DELETE /api/users/42:删除用户

资源名称使用复数名词,层级关系用斜杠表达,如 /api/users/42/orders 表示某个用户的订单列表。

2.2 HTTP 方法与语义

每个 HTTP 方法对应一种标准操作:

方法 语义 幂等
GET 查询资源 是
POST 创建资源 否
PUT 整体替换资源 是
PATCH 局部更新资源 否
DELETE 删除资源 是

幂等意味着多次执行结果一致,这对网络重试场景非常重要。

2.3 无状态与统一接口

服务端不保存客户端会话状态,每个请求都携带完整信息,这便于水平扩展。同时,所有资源通过统一的接口访问,使用标准状态码表达结果,使用标准媒体类型(如 JSON)表达数据。

3. 使用 FastRoute 定义路由

FastRoute 是 PHP 社区广泛使用的轻量路由库,性能优异,支持占位符与分组。首先通过 Composer 安装:

bash 复制代码
composer require nikic/fast-route

3.1 基础路由配置

创建一个 routes.php 文件,集中定义所有路由:

php 复制代码
<?php
// routes.php
use FastRoute\RouteCollector;

return function (RouteCollector $r) {
    // 用户资源
    $r->addRoute('GET', '/api/users', 'UserController@index');
    $r->addRoute('POST', '/api/users', 'UserController@store');
    $r->addRoute('GET', '/api/users/{id:\d+}', 'UserController@show');
    $r->addRoute('PUT', '/api/users/{id:\d+}', 'UserController@update');
    $r->addRoute('DELETE', '/api/users/{id:\d+}', 'UserController@destroy');

    // 订单资源
    $r->addRoute('GET', '/api/users/{id:\d+}/orders', 'OrderController@index');
};

{id:\d+} 是带正则约束的占位符,确保 id 只能是数字,避免非法参数进入业务层。

3.2 前端控制器(入口文件)

在 public/index.php 中统一调度:

php 复制代码
<?php
// public/index.php
require __DIR__ . '/../vendor/autoload.php';

$dispatcher = FastRoute\simpleDispatcher(require __DIR__ . '/../routes.php');

$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];

// 去除查询字符串与末尾斜杠
if (false !== $pos = strpos($uri, '?')) {
    $uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);

$routeInfo = $dispatcher->dispatch($httpMethod, $uri);

switch ($routeInfo[0]) {
    case FastRoute\Dispatcher::NOT_FOUND:
        // 404 处理
        break;
    case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
        // 405 处理
        break;
    case FastRoute\Dispatcher::FOUND:
        $handler = $routeInfo[1];
        $vars = $routeInfo[2];
        // 调用控制器
        break;
}

4. 请求解析与 JSON 响应封装

4.1 请求体解析

REST API 通常接收 JSON 格式的请求体。我们需要一个统一的请求解析器:

php 复制代码
<?php
// src/Http/Request.php
namespace App\Http;

class Request
{
    public static function jsonBody(): array
    {
        $raw = file_get_contents('php://input');
        $data = json_decode($raw, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            return [];
        }

        return is_array($data) ? $data : [];
    }

    public static function queryParams(): array
    {
        return $_GET;
    }

    public static function header(string $name): ?string
    {
        $key = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
        return $_SERVER[$key] ?? null;
    }
}

4.2 JSON 响应封装

统一的响应结构能让客户端解析逻辑保持一致。我们设计一个 Response 类:

php 复制代码
<?php
// src/Http/Response.php
namespace App\Http;

class Response
{
    public static function json(array $data, int $status = 200): void
    {
        http_response_code($status);
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($data, JSON_UNESCAPED_UNICODE);
        exit;
    }

    public static function success($data = null, string $message = 'ok'): void
    {
        self::json([
            'code' => 0,
            'message' => $message,
            'data' => $data,
        ], 200);
    }

    public static function error(string $message, int $status, int $code = -1): void
    {
        self::json([
            'code' => $code,
            'message' => $message,
            'data' => null,
        ], $status);
    }
}

统一响应格式为 { code, message, data },其中 code 为业务码,HTTP 状态码 表达传输层结果,二者解耦,便于客户端分别处理。

5. 状态码与错误处理规范

5.1 常用状态码语义

状态码 含义 典型场景
200 成功 GET 查询、PUT 更新
201 已创建 POST 新建资源
204 无内容 DELETE 删除成功
400 请求参数错误 JSON 解析失败、校验不通过
401 未认证 缺少或无效的 Token
403 无权限 已认证但无权访问
404 资源不存在 路由未匹配或资源缺失
405 方法不允许 URL 存在但方法不支持
422 语义错误 业务校验失败
500 服务器内部错误 未捕获异常

5.2 全局异常处理

在入口文件中注册异常处理器,避免错误信息直接暴露给客户端:

php 复制代码
<?php
// public/index.php 中追加
set_exception_handler(function (Throwable $e) {
    error_log($e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());

    Response::error('服务器内部错误', 500);
});

生产环境务必隐藏堆栈细节,仅记录到日志;开发环境可选择性输出调试信息。

5.3 路由未匹配处理

完善入口文件中的 404 与 405 分支:

php 复制代码
case FastRoute\Dispatcher::NOT_FOUND:
    Response::error('资源不存在', 404);
    break;

case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
    Response::error('请求方法不允许', 405);
    break;

6. 简单鉴权:API Key / Bearer Token

6.1 API Key 鉴权

API Key 适合服务端到服务端的调用,客户端在请求头中携带密钥:

php 复制代码
<?php
// src/Auth/ApiKeyAuth.php
namespace App\Auth;

use App\Http\Request;
use App\Http\Response;

class ApiKeyAuth
{
    private const VALID_KEYS = [
        'live_key_abc123',
        'test_key_xyz789',
    ];

    public static function verify(): void
    {
        $key = Request::header('X-API-Key');

        if (!$key || !in_array($key, self::VALID_KEYS, true)) {
            Response::error('无效的 API Key', 401);
        }
    }
}

调用方在请求头中携带:

http 复制代码
GET /api/users HTTP/1.1
Host: api.example.com
X-API-Key: live_key_abc123

6.2 Bearer Token 鉴权

Bearer Token 更常用于用户态场景,客户端在 Authorization 头中携带令牌:

php 复制代码
<?php
// src/Auth/BearerAuth.php
namespace App\Auth;

use App\Http\Request;
use App\Http\Response;

class BearerAuth
{
    public static function verify(): void
    {
        $header = Request::header('Authorization');

        if (!$header || !preg_match('/^Bearer\s+(\S+)$/', $header, $matches)) {
            Response::error('缺少或格式错误的 Token', 401);
        }

        $token = $matches[1];

        // 实际项目中应校验签名与过期时间,这里仅做示例
        if (!self::isValid($token)) {
            Response::error('Token 无效或已过期', 401);
        }
    }

    private static function isValid(string $token): bool
    {
        // 可对接 Redis / 数据库 / JWT 验签
        return $token === 'demo_token_2024';
    }
}

请求示例:

http 复制代码
GET /api/users/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer demo_token_2024

6.3 在控制器中组合使用

php 复制代码
<?php
// src/Controller/UserController.php
namespace App\Controller;

use App\Auth\ApiKeyAuth;
use App\Auth\BearerAuth;
use App\Http\Response;

class UserController
{
    public function index(array $params): void
    {
        ApiKeyAuth::verify(); // 服务端调用场景
        // 业务逻辑...
        Response::success([['id' => 1, 'name' => 'Alice']]);
    }

    public function show(array $params): void
    {
        BearerAuth::verify(); // 用户态场景
        $id = (int) $params['id'];
        // 业务逻辑...
        Response::success(['id' => $id, 'name' => 'Alice']);
    }
}

7. 完整请求流程串联

将以上模块在入口文件中串联起来,形成完整的请求生命周期:

php 复制代码
<?php
// public/index.php 完整版
require __DIR__ . '/../vendor/autoload.php';

use App\Auth\ApiKeyAuth;
use App\Auth\BearerAuth;
use App\Http\Response;
use FastRoute\Dispatcher;

$dispatcher = FastRoute\simpleDispatcher(require __DIR__ . '/../routes.php');

$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];

if (false !== $pos = strpos($uri, '?')) {
    $uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);

$routeInfo = $dispatcher->dispatch($httpMethod, $uri);

switch ($routeInfo[0]) {
    case Dispatcher::NOT_FOUND:
        Response::error('资源不存在', 404);
        break;

    case Dispatcher::METHOD_NOT_ALLOWED:
        Response::error('请求方法不允许', 405);
        break;

    case Dispatcher::FOUND:
        [$controller, $method] = explode('@', $routeInfo[1]);
        $vars = $routeInfo[2];

        $instance = new ("App\\Controller\\{$controller}")();
        $instance->{$method}($vars);
        break;
}

8. 总结

本文用 PHP 原生能力配合 FastRoute,搭建了一个轻量级 REST API 骨架,覆盖了从路由、请求解析、响应封装到鉴权的完整链路。核心要点如下:

  • REST 设计:资源化 URL、语义化 HTTP 方法、无状态通信。
  • 路由:FastRoute 提供高性能匹配与参数约束。
  • 响应封装 :统一 { code, message, data } 结构,业务码与状态码解耦。
  • 错误处理:全局异常捕获,生产环境隐藏堆栈。
  • 鉴权:API Key 适合机器间调用,Bearer Token 适合用户态场景。

这套骨架足够支撑中小型项目的起步,后续可按需扩展中间件、参数校验、数据库访问层与日志系统。保持轻量、保持清晰,是构建可维护 API 的长久之道。

相关推荐
思无邪663 小时前
用 AI 做 JS 逆向:从抓包到复现的完整方法论
开发语言·javascript·人工智能
H.莓飛4 小时前
【数据结构】二叉树_OJ题
linux·开发语言·数据结构·算法
零基础1235 小时前
Ubuntu 常用命令汇总
linux·运维·开发语言
外收内放5 小时前
Python基础语法练习题(57-58)
开发语言·python
时间的拾荒人5 小时前
Qt 界面美化实战:QSS 样式表
开发语言·qt·面试
码事漫谈6 小时前
三步改掉 AI 味,附可直接复制的去 AI 味提示词
后端
杨运交6 小时前
[076][核心模块]构建优雅的Java异常处理框架:从错误码到全局异常处理
java·开发语言
IT_陈寒6 小时前
Redis卡顿的锅,这次真不是大key的错
前端·人工智能·后端
可乐鸡翅yeah_6 小时前
video.js 集成 hls.js 开发 M3U8 播放器,新手高频踩坑
开发语言·前端·javascript·后端·ecmascript·m3u8·音视频在线播放