RESTful API,是现代Web开发中不可或缺的技术。无论是前后端分离、移动端开发、第三方服务对接,还是微服务架构,都离不开RESTful API。
作为一个PHP开发者,设计和实现RESTful API,是必备的技能。一个好的RESTful API,应该是简洁的、规范的、易用的、可扩展的、安全的。但在实际开发中,很多API设计得很糟糕,不规范,难用,难以维护。
今天,我们来详细学习PHP RESTful API的设计,从理论到实践,从REST的基本概念到API的设计原则,从路由设计到数据格式,从认证授权到错误处理,帮你掌握RESTful API的设计和实现。
什么是REST
REST,全称Representational State Transfer(表述性状态转移),是一种软件架构风格,由Roy Fielding在2000年的博士论文中提出。
REST不是一个标准,而是一种设计风格,一组约束和原则。符合REST约束和原则的架构,就称为RESTful架构。
REST的核心概念:
1. 资源(Resource)
REST中,一切皆资源。资源是网络上的一个实体,可以是一篇文章、一个用户、一张图片、一个订单等。
每个资源,都有一个唯一的URI(统一资源标识符)来标识。比如:
/articles/123:标识id为123的文章/users/456:标识id为456的用户/categories/789:标识id为789的分类
2. 表述(Representation)
资源的表述,是资源在某个时刻的状态的表示。资源可以有多种表述形式,如JSON、XML、HTML等。
客户端和服务器之间,传递的是资源的表述,而不是资源本身。客户端请求一个资源,服务器返回该资源的表述(如JSON格式的数据);客户端修改一个资源,发送该资源的表述给服务器。
3. 状态转移(State Transfer)
状态转移,是指通过HTTP方法,对资源进行操作,改变资源的状态。
REST使用HTTP方法来表示对资源的操作:
- GET:获取资源(查询)
- POST:创建资源
- PUT:更新资源(全量更新)
- PATCH:更新资源(部分更新)
- DELETE:删除资源
通过这些HTTP方法,客户端可以对资源进行增删改查操作,实现资源状态的转移。
4. 无状态(Stateless)
REST是无状态的。服务器不保存客户端的状态,每个请求都是独立的,包含了处理该请求所需的所有信息。
无状态的好处:
- 可扩展性好:服务器不需要维护会话,可以轻松扩展到多台服务器
- 可靠性高:一个请求失败,不影响其他请求
- 性能好:服务器不需要保存和恢复会话状态
5. 统一接口(Uniform Interface)
REST的统一接口,是指通过统一的HTTP方法和URI,对资源进行操作。不管是什么资源,都使用相同的HTTP方法(GET/POST/PUT/PATCH/DELETE)和相同的URI设计风格。
统一接口的好处:
- 简单易用:开发者不需要学习不同的接口风格
- 可预测性强:看到URI和HTTP方法,就知道要做什么
- 解耦:客户端和服务器解耦,各自可以独立演化
REST的6个约束
Roy Fielding提出了REST的6个约束,符合这些约束的架构,才是真正的RESTful架构:
1. 客户端-服务器分离(Client-Server)
客户端和服务器分离,各自独立演化。客户端负责用户界面和用户交互,服务器负责数据存储和业务逻辑。
好处:关注点分离,提高了系统的可扩展性和可移植性。
2. 无状态(Stateless)
服务器不保存客户端的状态,每个请求都是独立的,包含了处理该请求所需的所有信息。
好处:可扩展性好,可靠性高,性能好。
3. 可缓存(Cacheable)
服务器的响应,可以被客户端缓存。缓存可以减少客户端和服务器之间的交互,提高性能和可扩展性。
服务器通过HTTP缓存头(如Cache-Control、ETag、Last-Modified)来控制缓存。
4. 统一接口(Uniform Interface)
通过统一的HTTP方法和URI,对资源进行操作。这是REST最核心的约束。
统一接口包含4个子约束:
- 资源的标识(Resource identification in requests):通过URI标识资源
- 通过表述操作资源(Resource manipulation through representations):通过资源的表述来操作资源
- 自描述的消息(Self-descriptive messages):每个消息包含了足够的信息来描述如何处理它
- 超媒体作为应用状态的引擎(Hypermedia as the engine of application state, HATEOAS):客户端通过服务器返回的超媒体链接,来发现和导航可用的操作
5. 分层系统(Layered System)
系统可以分层,客户端无法直接感知中间层(如代理、网关、负载均衡)。中间层可以提高系统的可扩展性、安全性和性能。
6. 按需代码(Code on Demand,可选)
服务器可以向客户端传输代码(如JavaScript),客户端执行这些代码,扩展客户端的功能。这是一个可选的约束。
RESTful API设计原则
设计一个好的RESTful API,应该遵循以下原则:
1. 使用名词,不使用动词
URI应该使用名词,表示资源,而不是使用动词,表示操作。
因为HTTP方法已经表示了操作(GET=查询、POST=创建、PUT=更新、DELETE=删除),URI不需要再包含动词。
好的设计:
- GET
/articles:获取文章列表 - GET
/articles/123:获取id为123的文章 - POST
/articles:创建文章 - PUT
/articles/123:更新id为123的文章 - DELETE
/articles/123:删除id为123的文章
不好的设计:
- GET
/getArticles:获取文章列表 - GET
/getArticle?id=123:获取id为123的文章 - POST
/createArticle:创建文章 - POST
/updateArticle?id=123:更新id为123的文章 - POST
/deleteArticle?id=123:删除id为123的文章
2. 使用复数名词
URI中的资源名,应该使用复数名词。
因为资源通常是一个集合(如文章集合、用户集合),使用复数更符合直觉。
好的设计:
/articles:文章集合/articles/123:文章集合中的id为123的文章/users:用户集合/users/456:用户集合中的id为456的用户
不好的设计:
/article/article/123/user/user/456
3. 层级关系
如果资源之间有层级关系(从属关系),应该在URI中体现出来。
比如,文章的评论,是从属于文章的;用户的订单,是从属于用户的。
好的设计:
/articles/123/comments:文章123的评论列表/articles/123/comments/456:文章123的id为456的评论/users/456/orders:用户456的订单列表/users/456/orders/789:用户456的id为789的订单
4. 使用HTTP方法表示操作
使用HTTP方法来表示对资源的操作,不要用URI或参数来表示操作。
| HTTP方法 | 操作 | 幂等 | 安全 |
|---|---|---|---|
| GET | 获取资源 | 是 | 是 |
| POST | 创建资源 | 否 | 否 |
| PUT | 全量更新资源 | 是 | 否 |
| PATCH | 部分更新资源 | 否 | 否 |
| DELETE | 删除资源 | 是 | 否 |
- 安全:不会改变服务器状态的方法(GET)
- 幂等:执行多次和执行一次效果相同的方法(GET、PUT、DELETE)
5. 使用HTTP状态码
使用HTTP状态码来表示请求的结果,不要在响应体中用自定义的状态码。
常用的HTTP状态码:
2xx 成功:
- 200 OK:请求成功(GET、PUT、PATCH)
- 201 Created:资源创建成功(POST)
- 204 No Content:请求成功,但没有返回内容(DELETE)
3xx 重定向:
- 301 Moved Permanently:永久重定向
- 302 Found:临时重定向
- 304 Not Modified:资源未修改(缓存)
4xx 客户端错误:
- 400 Bad Request:请求参数错误
- 401 Unauthorized:未认证
- 403 Forbidden:已认证,但无权限
- 404 Not Found:资源不存在
- 405 Method Not Allowed:HTTP方法不允许
- 409 Conflict:资源冲突(如重复创建)
- 410 Gone:资源已永久删除
- 415 Unsupported Media Type:不支持的媒体类型
- 422 Unprocessable Entity:请求格式正确,但语义错误(如验证失败)
- 429 Too Many Requests:请求过于频繁(限流)
5xx 服务器错误:
- 500 Internal Server Error:服务器内部错误
- 502 Bad Gateway:网关错误
- 503 Service Unavailable:服务不可用
- 504 Gateway Timeout:网关超时
6. 统一的数据格式
API的请求和响应,应该使用统一的数据格式,推荐使用JSON。
JSON的优点:
- 简洁,易读
- 支持多种语言
- 解析快
- 是Web API的事实标准
响应格式应该统一,包含:
- 数据(data):请求的数据
- 元信息(meta):分页、总数等元信息
- 错误信息(error):错误时的错误信息
成功响应示例:
{
"code": 0,
"message": "success",
"data": {
"id": 123,
"title": "文章标题",
"content": "文章内容",
"created_at": "2015-09-18 10:00:00"
}
}列表响应示例:
{
"code": 0,
"message": "success",
"data": [
{"id": 1, "title": "文章1"},
{"id": 2, "title": "文章2"}
],
"meta": {
"total": 100,
"page": 1,
"per_page": 10,
"total_pages": 10
}
}错误响应示例:
{
"code": 40001,
"message": "参数验证失败",
"errors": {
"title": ["标题不能为空", "标题长度不能超过200"],
"content": ["内容不能为空"]
}
}7. 版本控制
API应该有版本控制,以便在API不兼容升级时,旧版本的客户端仍然可以使用。
常见的版本控制方式:
URI中包含版本(推荐):
/api/v1/articles/api/v2/articles
优点:简单直观,易于路由 缺点:URI不那么"RESTful"
请求头中包含版本:
Accept: application/vnd.myapi.v1+jsonX-API-Version: 1
优点:URI干净 缺点:不直观,调试麻烦
查询参数中包含版本:
/api/articles?version=1
不推荐,不优雅。
推荐使用URI中包含版本的方式,简单直观。
8. 认证和授权
API应该有认证和授权机制,确保只有合法的用户才能访问,且只能访问有权限的资源。
认证方式:
- API Key:在请求头或参数中传递API Key,简单但安全性一般
- Basic Auth:用户名密码Base64编码,简单但不安全(需HTTPS)
- Bearer Token(JWT):JSON Web Token,无状态,适合RESTful API
- OAuth 2.0:第三方授权框架,适合开放平台
推荐使用JWT(JSON Web Token),无状态,适合RESTful API的无状态约束。
授权方式:
- 基于角色的访问控制(RBAC):用户有角色,角色有权限
- 基于资源的访问控制:每个资源有所有者,只能访问自己的资源
9. 分页、排序、过滤
列表接口,应该支持分页、排序、过滤。
分页:
/articles?page=1&per_page=10- 响应中包含分页元信息(total、page、perpage、totalpages)
排序:
/articles?sort=created_at&order=desc- 支持多字段排序:
/articles?sort=created_at,views&order=desc,asc
过滤:
/articles?category_id=2&status=published- 支持范围过滤:
/articles?createdatfrom=2015-01-01&createdatto=2015-12-31 - 支持搜索:
/articles?keyword=php
10. 错误处理
API应该有统一的错误处理,返回清晰的错误信息,帮助开发者定位问题。
错误响应应该包含:
- 错误码(code):自定义的错误码,便于程序处理
- 错误信息(message):人类可读的错误信息
- 错误详情(errors):字段级别的错误详情(验证失败时)
- 错误链接(documentation_url):可选,指向错误文档
11. 文档
API应该有完整的文档,帮助开发者快速上手。
文档应该包含:
- API概述
- 认证方式
- 每个接口的详细说明(URI、HTTP方法、参数、响应、错误码)
- 示例代码
- 常见问题
常用的API文档工具:
- Swagger/OpenAPI:标准的API文档规范,可自动生成文档和测试界面
- Apiary:API设计和文档工具
- Postman:API测试工具,也可以生成文档
- 手写Markdown文档:简单灵活
12. HATEOAS(可选)
HATEOAS(Hypermedia as the Engine of Application State),是REST的最高境界。客户端不需要知道API的URI结构,只需要知道入口URI,然后通过服务器返回的超媒体链接,来发现和导航可用的操作。
示例:
{
"id": 123,
"title": "文章标题",
"content": "文章内容",
"links": {
"self": {"href": "/api/v1/articles/123", "method": "GET"},
"update": {"href": "/api/v1/articles/123", "method": "PUT"},
"delete": {"href": "/api/v1/articles/123", "method": "DELETE"},
"comments": {"href": "/api/v1/articles/123/comments", "method": "GET"},
"author": {"href": "/api/v1/users/456", "method": "GET"}
}
}HATEOAS的好处:
- 客户端和服务器解耦,服务器可以自由修改URI结构
- API自描述,客户端可以动态发现可用的操作
- 减少客户端的硬编码
但HATEOAS实现复杂,大多数API都没有严格实现。可以根据需要,选择性地实现。
PHP实现RESTful API
下面,以我们的博客系统为例,演示如何用PHP实现RESTful API。
1. 项目结构
blog/
├── api/
│ ├── index.php # API入口
│ ├── config.php # 配置文件
│ ├── Database.php # 数据库类
│ ├── Router.php # 路由类
│ ├── JWT.php # JWT类
│ ├── Response.php # 响应类
│ ├── controllers/
│ │ ├── ArticleController.php
│ │ ├── CategoryController.php
│ │ ├── UserController.php
│ │ └── CommentController.php
│ └── models/
│ ├── Article.php
│ ├── Category.php
│ ├── User.php
│ └── Comment.php
└── ...2. 入口文件(api/index.php)
<?php
// API入口文件
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
// 处理OPTIONS预检请求
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/Router.php';
require_once __DIR__ . '/Response.php';
// 路由
$router = new Router();
// 文章路由
$router->get('/api/v1/articles', 'ArticleController@index');
$router->get('/api/v1/articles/{id}', 'ArticleController@show');
$router->post('/api/v1/articles', 'ArticleController@store');
$router->put('/api/v1/articles/{id}', 'ArticleController@update');
$router->delete('/api/v1/articles/{id}', 'ArticleController@destroy');
// 分类路由
$router->get('/api/v1/categories', 'CategoryController@index');
$router->get('/api/v1/categories/{id}', 'CategoryController@show');
// 用户路由
$router->post('/api/v1/auth/login', 'UserController@login');
$router->post('/api/v1/auth/register', 'UserController@register');
$router->get('/api/v1/user', 'UserController@profile');
// 评论路由
$router->get('/api/v1/articles/{articleId}/comments', 'CommentController@index');
$router->post('/api/v1/articles/{articleId}/comments', 'CommentController@store');
$router->delete('/api/v1/comments/{id}', 'CommentController@destroy');
// 执行路由
try {
$router->dispatch();
} catch (Exception $e) {
Response::error($e->getCode() ?: 500, $e->getMessage());
}3. 路由类(Router.php)
<?php
class Router {
private $routes = [];
public function get($pattern, $handler) {
$this->addRoute('GET', $pattern, $handler);
}
public function post($pattern, $handler) {
$this->addRoute('POST', $pattern, $handler);
}
public function put($pattern, $handler) {
$this->addRoute('PUT', $pattern, $handler);
}
public function patch($pattern, $handler) {
$this->addRoute('PATCH', $pattern, $handler);
}
public function delete($pattern, $handler) {
$this->addRoute('DELETE', $pattern, $handler);
}
private function addRoute($method, $pattern, $handler) {
// 将{param}转换为正则
$pattern = preg_replace('/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', '([^/]+)', $pattern);
$regex = '#^' . $pattern . '$#';
$this->routes[] = [
'method' => $method,
'regex' => $regex,
'handler' => $handler
];
}
public function dispatch() {
$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
foreach ($this->routes as $route) {
if ($route['method'] !== $method) continue;
if (preg_match($route['regex'], $uri, $matches)) {
// 提取参数
array_shift($matches);
$params = $matches;
// 解析handler
list($controllerName, $action) = explode('@', $route['handler']);
$controllerClass = $controllerName;
$controllerFile = __DIR__ . '/controllers/' . $controllerName . '.php';
if (!file_exists($controllerFile)) {
throw new Exception("Controller not found: $controllerName", 404);
}
require_once $controllerFile;
$controller = new $controllerClass();
if (!method_exists($controller, $action)) {
throw new Exception("Action not found: $action", 404);
}
// 调用方法
call_user_func_array([$controller, $action], $params);
return;
}
}
throw new Exception('Route not found', 404);
}
}4. 响应类(Response.php)
<?php
class Response {
public static function json($data, $code = 200) {
http_response_code($code);
echo json_encode([
'code' => 0,
'message' => 'success',
'data' => $data
], JSON_UNESCAPED_UNICODE);
exit;
}
public static function success($data = null, $message = 'success') {
echo json_encode([
'code' => 0,
'message' => $message,
'data' => $data
], JSON_UNESCAPED_UNICODE);
exit;
}
public static function error($code, $message, $errors = null, $httpCode = 400) {
http_response_code($httpCode);
$response = [
'code' => $code,
'message' => $message
];
if ($errors !== null) {
$response['errors'] = $errors;
}
echo json_encode($response, JSON_UNESCAPED_UNICODE);
exit;
}
public static function paginate($items, $total, $page, $perPage) {
echo json_encode([
'code' => 0,
'message' => 'success',
'data' => $items,
'meta' => [
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'total_pages' => ceil($total / $perPage)
]
], JSON_UNESCAPED_UNICODE);
exit;
}
public static function created($data) {
http_response_code(201);
echo json_encode([
'code' => 0,
'message' => 'created',
'data' => $data
], JSON_UNESCAPED_UNICODE);
exit;
}
public static function noContent() {
http_response_code(204);
exit;
}
}5. 文章控制器(ArticleController.php)
<?php
require_once __DIR__ . '/../models/Article.php';
require_once __DIR__ . '/../Response.php';
class ArticleController {
private $articleModel;
public function __construct() {
$this->articleModel = new Article();
}
// GET /api/v1/articles
public function index() {
$page = max(1, intval($_GET['page'] ?? 1));
$perPage = min(100, max(1, intval($_GET['per_page'] ?? 10)));
$categoryId = intval($_GET['category_id'] ?? 0);
$keyword = trim($_GET['keyword'] ?? '');
$sort = $_GET['sort'] ?? 'created_at';
$order = strtoupper($_GET['order'] ?? 'DESC');
$allowedSorts = ['created_at', 'views', 'id'];
if (!in_array($sort, $allowedSorts)) $sort = 'created_at';
if (!in_array($order, ['ASC', 'DESC'])) $order = 'DESC';
$result = $this->articleModel->getList($page, $perPage, $categoryId, $keyword, $sort, $order);
Response::paginate($result['items'], $result['total'], $page, $perPage);
}
// GET /api/v1/articles/{id}
public function show($id) {
$article = $this->articleModel->getById($id);
if (!$article) {
Response::error(40401, '文章不存在', null, 404);
}
// 增加阅读量
$this->articleModel->incrementViews($id);
Response::success($article);
}
// POST /api/v1/articles
public function store() {
// 认证检查
$this->requireAuth();
$input = json_decode(file_get_contents('php://input'), true);
// 验证
$errors = [];
if (empty($input['title'])) $errors['title'][] = '标题不能为空';
if (mb_strlen($input['title'] ?? '') > 200) $errors['title'][] = '标题长度不能超过200';
if (empty($input['content'])) $errors['content'][] = '内容不能为空';
if (empty($input['category_id'])) $errors['category_id'][] = '分类不能为空';
if (!empty($errors)) {
Response::error(40001, '参数验证失败', $errors, 422);
}
$article = $this->articleModel->create($input);
Response::created($article);
}
// PUT /api/v1/articles/{id}
public function update($id) {
$this->requireAuth();
$article = $this->articleModel->getById($id);
if (!$article) {
Response::error(40401, '文章不存在', null, 404);
}
$input = json_decode(file_get_contents('php://input'), true);
$updated = $this->articleModel->update($id, $input);
Response::success($updated);
}
// DELETE /api/v1/articles/{id}
public function destroy($id) {
$this->requireAuth();
$article = $this->articleModel->getById($id);
if (!$article) {
Response::error(40401, '文章不存在', null, 404);
}
$this->articleModel->delete($id);
Response::noContent();
}
private function requireAuth() {
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!preg_match('/Bearer\s+(.*)/', $authHeader, $matches)) {
Response::error(40101, '未认证', null, 401);
}
$token = $matches[1];
// 验证JWT token
// ...
}
}6. 文章模型(Article.php)
<?php
require_once __DIR__ . '/../Database.php';
class Article {
private $db;
public function __construct() {
$this->db = Database::getInstance();
}
public function getList($page, $perPage, $categoryId = 0, $keyword = '', $sort = 'created_at', $order = 'DESC') {
$offset = ($page - 1) * $perPage;
$where = "WHERE status = 'published'";
$params = [];
if ($categoryId > 0) {
$where .= " AND category_id = ?";
$params[] = $categoryId;
}
if ($keyword) {
$where .= " AND (title LIKE ? OR content LIKE ?)";
$params[] = "%$keyword%";
$params[] = "%$keyword%";
}
// 总数
$countSql = "SELECT COUNT(*) FROM blog_posts $where";
$stmt = $this->db->prepare($countSql);
$stmt->execute($params);
$total = $stmt->fetchColumn();
// 列表
$sql = "SELECT id, title, slug, excerpt, cover, category_id, views, created_at
FROM blog_posts
$where
ORDER BY $sort $order
LIMIT ? OFFSET ?";
$stmt = $this->db->prepare($sql);
$stmt->execute(array_merge($params, [$perPage, $offset]));
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
return ['items' => $items, 'total' => $total];
}
public function getById($id) {
$sql = "SELECT * FROM blog_posts WHERE id = ? AND status = 'published'";
$stmt = $this->db->prepare($sql);
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function create($data) {
$slug = $data['slug'] ?? $this->generateSlug($data['title']);
$excerpt = $data['excerpt'] ?? mb_substr(strip_tags($data['content']), 0, 200);
$now = date('Y-m-d H:i:s');
$sql = "INSERT INTO blog_posts (title, slug, content, excerpt, category_id, cover, tags, status, views, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, 'published', 0, ?, ?)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
$data['title'], $slug, $data['content'], $excerpt,
$data['category_id'], $data['cover'] ?? '', $data['tags'] ?? '',
$now, $now
]);
$id = $this->db->lastInsertId();
return $this->getById($id);
}
public function update($id, $data) {
$fields = [];
$params = [];
$allowed = ['title', 'content', 'excerpt', 'category_id', 'cover', 'tags', 'status'];
foreach ($allowed as $field) {
if (isset($data[$field])) {
$fields[] = "$field = ?";
$params[] = $data[$field];
}
}
if (empty($fields)) return $this->getById($id);
$fields[] = "updated_at = ?";
$params[] = date('Y-m-d H:i:s');
$params[] = $id;
$sql = "UPDATE blog_posts SET " . implode(', ', $fields) . " WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $this->getById($id);
}
public function delete($id) {
$sql = "UPDATE blog_posts SET status = 'trash' WHERE id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$id]);
}
public function incrementViews($id) {
$sql = "UPDATE blog_posts SET views = views + 1 WHERE id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$id]);
}
private function generateSlug($title) {
// 简单的slug生成,实际可以用拼音转换
return 'post-' . time();
}
}7. Nginx配置
server {
listen 80;
server_name api.example.com;
root /var/www/blog/api;
index index.php;
# URL重写,所有请求都走index.php
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP处理
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# 缓存静态文件
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# 安全头
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
# 限流
limit_req zone=api burst=20 nodelay;
}API安全
1. HTTPS
API必须使用HTTPS,确保数据传输的安全性。不要在HTTP上传输敏感信息(如密码、Token)。
2. 认证
使用JWT或OAuth 2.0进行认证,不要使用Cookie/Session(REST是无状态的)。
3. 授权
实现基于角色或资源的授权,确保用户只能访问有权限的资源。
4. 输入验证
对所有输入进行验证,防止SQL注入、XSS、CSRF等攻击。
- 使用PDO预处理语句,防止SQL注入
- 对输出进行HTML转义,防止XSS
- 使用Token验证,防止CSRF(API通常用JWT,不需要CSRF)
5. 限流
实现API限流,防止滥用和DDoS攻击。可以用Nginx的limit_req,或应用层的限流(如Redis实现的令牌桶算法)。
6. 敏感信息
不要在响应中返回敏感信息(如密码、密码哈希、内部ID等)。
7. 错误信息
不要在错误信息中暴露服务器内部信息(如文件路径、数据库错误详情等),防止信息泄露。
API性能优化
1. 缓存
对不常变化的数据进行缓存,如文章列表、分类列表等。可以用Redis缓存,减少数据库查询。
2. 分页
列表接口必须分页,避免一次返回太多数据,影响性能。
3. 字段选择
支持字段选择,客户端可以只请求需要的字段,减少数据传输量。
/articles?fields=id,title,created_at
4. 关联数据
避免N+1查询,使用JOIN或预加载,减少数据库查询次数。
5. 压缩
启用Gzip压缩,减少响应数据的大小。
6. 数据库优化
合理使用索引,优化SQL查询,避免慢查询。
总结
RESTful API设计,是现代Web开发的重要技能。
核心要点:
- 什么是REST:表述性状态转移,一种软件架构风格,核心概念是资源、表述、状态转移、无状态、统一接口
- REST的6个约束:客户端-服务器分离、无状态、可缓存、统一接口、分层系统、按需代码(可选)
- 设计原则:使用名词不使用动词、使用复数名词、层级关系、使用HTTP方法表示操作、使用HTTP状态码、统一数据格式(JSON)、版本控制、认证授权、分页排序过滤、统一错误处理、文档、HATEOAS(可选)
- PHP实现:入口文件、路由类、响应类、控制器、模型、Nginx配置
- API安全:HTTPS、认证(JWT)、授权、输入验证、限流、敏感信息保护、错误信息保护
- 性能优化:缓存、分页、字段选择、关联数据优化、压缩、数据库优化
一个好的RESTful API,应该是简洁的、规范的、易用的、可扩展的、安全的、高性能的。设计API时,要站在使用者的角度,考虑易用性和可维护性,遵循REST的原则和最佳实践。
"好的API,是好的产品的一半。"希望这篇文章,能帮你掌握RESTful API的设计和实现,设计出简洁、规范、易用的API。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录