PHP API Gateway 的构建:统一认证、限流与请求路由的实现指南
大家好,今天我们来聊聊如何使用 PHP 构建一个 API Gateway,重点关注统一认证、限流和请求路由这三个核心功能。API Gateway 作为微服务架构中的重要组成部分,负责处理所有外部请求,并将它们路由到相应的后端服务。它能够极大地简化客户端的开发,提高系统的安全性、可维护性和可扩展性。
一、API Gateway 的作用与优势
在深入代码之前,我们先来了解一下 API Gateway 的作用和优势:
- 统一入口: 将多个后端服务暴露为一个统一的入口点,客户端无需关心后端服务的具体地址。
- 认证与授权: 集中处理身份验证和授权,确保只有经过授权的请求才能访问后端服务。
- 限流: 防止恶意请求或意外流量峰值导致后端服务崩溃,保障系统的稳定性。
- 请求路由: 根据请求的 URL、Header 或其他信息,将请求路由到相应的后端服务。
- 协议转换: 可以在不同的协议之间进行转换,例如将 REST 请求转换为 gRPC 请求。
- 监控与日志: 集中收集请求的监控数据和日志,方便进行性能分析和故障排查。
二、技术选型
本次实践中,我们将采用以下技术:
- PHP 8+: 使用最新的 PHP 版本,获得更好的性能和语言特性。
- Composer: PHP 的依赖管理工具。
- Slim Framework: 一个轻量级的 PHP 微框架,用于快速构建 API Gateway 的核心逻辑。
- Redis: 用于存储会话信息和实现限流。
- Guzzle HTTP Client: 用于向后端服务发送请求。
三、环境准备
首先,确保你已经安装了 PHP 8+、Composer 和 Redis。
-
创建项目目录:
mkdir api-gateway cd api-gateway -
初始化 Composer:
composer init根据提示填写项目信息,或者直接按回车键使用默认值。
-
安装依赖:
composer require slim/slim:"^4" slim/psr7 guzzlehttp/guzzle predis/predis这个命令会安装 Slim Framework、PSR-7 兼容库、Guzzle HTTP Client 和 Predis (Redis client)。
四、项目结构
推荐的项目结构如下:
api-gateway/
├── src/
│ ├── Middleware/
│ │ ├── AuthenticationMiddleware.php // 认证中间件
│ │ ├── RateLimitMiddleware.php // 限流中间件
│ ├── RouteConfig.php // 路由配置
│ ├── Config.php // 全局配置
├── public/
│ └── index.php // 入口文件
├── composer.json
├── .env // 环境变量文件
└── .htaccess // Apache 配置 (可选)
五、代码实现
- .env 文件
首先,创建 .env 文件,用于存储配置信息:
API_KEY_HEADER=X-API-Key
API_KEY=your_secret_api_key
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
RATE_LIMIT_WINDOW=60 # 单位:秒
RATE_LIMIT_MAX_REQUESTS=100
- Config.php
创建 src/Config.php 文件,用于读取配置信息:
<?php
namespace App;
use DotenvDotenv;
class Config
{
private static $instance;
private $config = [];
private function __construct()
{
$dotenv = Dotenv::createImmutable(__DIR__ . '/../');
$dotenv->load();
$this->config = $_ENV;
}
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function get(string $key, $default = null)
{
return $this->config[$key] ?? $default;
}
}
- AuthenticationMiddleware.php
创建 src/Middleware/AuthenticationMiddleware.php 文件,实现认证中间件:
<?php
namespace AppMiddleware;
use PsrHttpMessageServerRequestInterface as Request;
use PsrHttpServerRequestHandlerInterface as Handler;
use SlimPsr7Response;
use AppConfig;
class AuthenticationMiddleware
{
public function __invoke(Request $request, Handler $handler): Response
{
$config = Config::getInstance();
$apiKeyHeader = $config->get('API_KEY_HEADER');
$apiKey = $config->get('API_KEY');
$headerValue = $request->getHeaderLine($apiKeyHeader);
if (empty($headerValue) || $headerValue !== $apiKey) {
$response = new Response();
$response->getBody()->write(json_encode(['error' => 'Unauthorized'], JSON_UNESCAPED_UNICODE));
return $response->withStatus(401)->withHeader('Content-Type', 'application/json');
}
return $handler->handle($request);
}
}
- RateLimitMiddleware.php
创建 src/Middleware/RateLimitMiddleware.php 文件,实现限流中间件:
<?php
namespace AppMiddleware;
use PsrHttpMessageServerRequestInterface as Request;
use PsrHttpServerRequestHandlerInterface as Handler;
use SlimPsr7Response;
use PredisClient;
use AppConfig;
class RateLimitMiddleware
{
private $redis;
public function __construct(Client $redis)
{
$this->redis = $redis;
}
public function __invoke(Request $request, Handler $handler): Response
{
$config = Config::getInstance();
$redisKeyPrefix = 'rate_limit:' . md5($request->getServerParams()['REMOTE_ADDR']); // 使用 IP 地址作为 key
$rateLimitWindow = (int)$config->get('RATE_LIMIT_WINDOW');
$rateLimitMaxRequests = (int)$config->get('RATE_LIMIT_MAX_REQUESTS');
$requestCount = $this->redis->incr($redisKeyPrefix);
if ($requestCount === 1) {
$this->redis->expire($redisKeyPrefix, $rateLimitWindow);
}
if ($requestCount > $rateLimitMaxRequests) {
$response = new Response();
$response->getBody()->write(json_encode(['error' => 'Too Many Requests'], JSON_UNESCAPED_UNICODE));
return $response->withStatus(429)->withHeader('Content-Type', 'application/json');
}
$response = $handler->handle($request);
return $response->withHeader('X-RateLimit-Limit', $rateLimitMaxRequests)
->withHeader('X-RateLimit-Remaining', $rateLimitMaxRequests - $requestCount)
->withHeader('X-RateLimit-Reset', time() + $this->redis->ttl($redisKeyPrefix));
}
}
- RouteConfig.php
创建 src/RouteConfig.php 文件,实现路由配置:
<?php
namespace App;
use SlimApp;
use SlimRoutingRouteCollectorProxy;
use GuzzleHttpClient;
use PsrHttpMessageResponseInterface as Response;
use PsrHttpMessageServerRequestInterface as Request;
use AppMiddlewareAuthenticationMiddleware;
use AppMiddlewareRateLimitMiddleware;
use PredisClient as RedisClient;
class RouteConfig
{
public static function configure(App $app)
{
$app->group('/api', function (RouteCollectorProxy $group) {
// 应用 Authentication 中间件
$group->group('', function (RouteCollectorProxy $groupInner) {
// 使用GuzzleHttp转发请求
$groupInner->any('/users/{path:.*}', function (Request $request, Response $response, array $args) {
$client = new Client([
'base_uri' => 'http://user-service:8081', // 替换为用户服务地址
'timeout' => 5.0,
]);
$method = $request->getMethod();
$path = '/users/' . $args['path'];
$queryParams = $request->getQueryParams();
$body = $request->getBody();
$headers = $request->getHeaders();
try {
$res = $client->request($method, $path, [
'query' => $queryParams,
'body' => $body,
'headers' => $headers,
'http_errors' => false // 不要抛出异常,允许处理非200状态码
]);
$response->getBody()->write($res->getBody());
return $response
->withStatus($res->getStatusCode())
->withHeaders($res->getHeaders());
} catch (Exception $e) {
$response->getBody()->write(json_encode(['error' => $e->getMessage()], JSON_UNESCAPED_UNICODE));
return $response->withStatus(500)->withHeader('Content-Type', 'application/json');
}
});
$groupInner->any('/products/{path:.*}', function (Request $request, Response $response, array $args) {
$client = new Client([
'base_uri' => 'http://product-service:8082', // 替换为产品服务地址
'timeout' => 5.0,
]);
$method = $request->getMethod();
$path = '/products/' . $args['path'];
$queryParams = $request->getQueryParams();
$body = $request->getBody();
$headers = $request->getHeaders();
try {
$res = $client->request($method, $path, [
'query' => $queryParams,
'body' => $body,
'headers' => $headers,
'http_errors' => false
]);
$response->getBody()->write($res->getBody());
return $response
->withStatus($res->getStatusCode())
->withHeaders($res->getHeaders());
} catch (Exception $e) {
$response->getBody()->write(json_encode(['error' => $e->getMessage()], JSON_UNESCAPED_UNICODE));
return $response->withStatus(500)->withHeader('Content-Type', 'application/json');
}
});
})->add(new AuthenticationMiddleware());
//应用 RateLimit 中间件
$group->group('', function (RouteCollectorProxy $groupInner) {
// 任何路由
$groupInner->get('/hello', function (Request $request, Response $response) {
$response->getBody()->write(json_encode(['message' => 'Hello, Rate Limited World!'], JSON_UNESCAPED_UNICODE));
return $response->withHeader('Content-Type', 'application/json');
});
})->add(new RateLimitMiddleware(new RedisClient([
'host' => Config::getInstance()->get('REDIS_HOST'),
'port' => Config::getInstance()->get('REDIS_PORT'),
])));
});
}
}
注意:
- 需要替换
http://user-service:8081和http://product-service:8082为实际的后端服务地址。 {path:.*}是 Slim 4 的路由参数语法,表示匹配任意字符,包括斜杠/,这使得我们可以将请求完整地转发到后端服务。http_errors设置为false允许处理非 200 状态码,并将其转发给客户端。- 这里使用
group进行中间件的嵌套,先进行认证,再进行限流。 - 使用IP地址进行限流,生产环境可能需要更细粒度的限流控制,例如针对用户ID。
- index.php
创建 public/index.php 文件,作为入口文件:
<?php
use SlimFactoryAppFactory;
use AppRouteConfig;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
// 添加路由配置
RouteConfig::configure($app);
// 添加中间件(注意顺序)
$app->addBodyParsingMiddleware(); // 解析请求体
$app->addRoutingMiddleware(); // 添加路由中间件
$app->addErrorMiddleware(true, true, true); // 添加错误处理中间件
$app->run();
- .htaccess (可选)
如果使用 Apache 服务器,可以创建 .htaccess 文件,将所有请求重定向到 index.php:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
</IfModule>
六、运行与测试
-
启动 Redis:
确保 Redis 服务器已经启动。
-
启动 PHP 内置服务器:
php -S localhost:8080 -t public -
测试认证:
发送一个没有
X-API-KeyHeader 的请求:curl -i http://localhost:8080/api/users/123应该会收到
401 Unauthorized错误。发送一个带有正确的
X-API-KeyHeader 的请求:curl -i -H "X-API-Key: your_secret_api_key" http://localhost:8080/api/users/123如果后端服务正常运行,应该会收到用户服务的响应。
-
测试限流:
在短时间内发送大量请求到
/api/hello:for i in {1..150}; do curl http://localhost:8080/api/hello; done超过
RATE_LIMIT_MAX_REQUESTS的请求应该会收到429 Too Many Requests错误。
七、代码示例
以下是一个完整的 docker-compose.yml 文件,用于启动 API Gateway 和两个模拟的后端服务:
version: "3.8"
services:
api-gateway:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
depends_on:
- redis
- user-service
- product-service
environment:
API_KEY_HEADER: X-API-Key
API_KEY: your_secret_api_key
REDIS_HOST: redis
REDIS_PORT: 6379
RATE_LIMIT_WINDOW: 60
RATE_LIMIT_MAX_REQUESTS: 100
volumes:
- ./:/var/www/html
user-service:
image: nginx:latest
ports:
- "8081:80"
volumes:
- ./mock/user-service:/usr/share/nginx/html
networks:
- mynetwork
product-service:
image: nginx:latest
ports:
- "8082:80"
volumes:
- ./mock/product-service:/usr/share/nginx/html
networks:
- mynetwork
redis:
image: redis:latest
ports:
- "6379:6379"
networks:
- mynetwork
networks:
mynetwork:
需要创建对应的 Dockerfile 文件,例如:
FROM php:8.2-fpm-alpine
RUN apk update && apk add --no-cache $PHPIZE_DEPS
RUN pecl install redis && docker-php-ext-enable redis
RUN docker-php-ext-install pdo pdo_mysql
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
WORKDIR /var/www/html
EXPOSE 8080
CMD ["php", "-S", "0.0.0.0:8080", "-t", "public"]
同时创建 mock 文件夹,其中包含 user-service 和 product-service 两个文件夹,分别用于模拟后端服务。例如,mock/user-service/users/123/index.html 内容可以为:
<html>
<head>
<title>User Service</title>
</head>
<body>
<h1>User ID: 123</h1>
</body>
</html>
mock/product-service/products/456/index.html 内容可以为:
<html>
<head>
<title>Product Service</title>
</head>
<body>
<h1>Product ID: 456</h1>
</body>
</html>
启动 Docker Compose:
docker-compose up -d
八、进阶话题
- 更灵活的路由规则: 可以使用更复杂的路由规则,例如根据请求方法、Header 或 Cookie 进行路由。
- 请求转换与聚合: 可以对请求进行转换,例如修改请求头、请求体或 URL。还可以将多个请求聚合为一个请求,减少客户端的请求次数。
- 服务发现: 可以使用服务发现机制,例如 Consul 或 Eureka,动态地获取后端服务的地址。
- 熔断与降级: 当后端服务出现故障时,可以进行熔断或降级,防止雪崩效应。
- 监控与告警: 集成监控系统,例如 Prometheus 和 Grafana,实时监控 API Gateway 的性能指标,并设置告警规则。
- 使用OpenAPI规范: 可以使用OpenAPI规范(Swagger)定义API接口,并使用工具生成API文档和客户端代码。
- 安全加固: 除了API Key认证,可以考虑使用OAuth 2.0、JWT等更安全的认证方式。同时,需要对API Gateway进行安全加固,防止SQL注入、XSS等安全漏洞。
九、总结:API Gateway 的关键实现点
通过上述步骤,我们成功构建了一个简单的 PHP API Gateway,实现了统一认证、限流和请求路由的功能。这只是一个基础示例,你可以根据实际需求进行扩展和优化。构建API Gateway的核心在于路由转发和中间件的应用。通过灵活的路由配置,可以将请求转发到不同的后端服务。通过中间件,可以实现各种功能,例如认证、授权、限流、日志记录等。
希望今天的分享对你有所帮助!