admin管理员组

文章数量:1029102

ThinkPHP6 API开放平台:调用日志与请求频率限制的实现

在构建API开放平台时,调用日志记录和请求频率限制是两个至关重要的功能。调用日志帮助我们追踪API使用情况、排查问题,而频率限制则保护系统免受滥用和过载。本文将详细介绍如何在ThinkPHP6中实现这两大功能。

一、调用日志的实现

1.1 数据库设计

首先我们需要设计一个日志表来存储API调用记录:

代码语言:javascript代码运行次数:0运行复制
CREATE TABLE `api_call_logs` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `app_id` varchar(32) NOT NULL COMMENT '应用ID',
  `api_path` varchar(255) NOT NULL COMMENT 'API路径',
  `request_method` varchar(10) NOT NULL COMMENT '请求方法',
  `request_params` text COMMENT '请求参数',
  `response_code` int(11) NOT NULL COMMENT '响应状态码',
  `response_data` text COMMENT '响应数据',
  `ip_address` varchar(45) NOT NULL COMMENT 'IP地址',
  `user_agent` varchar(255) DEFAULT NULL COMMENT '用户代理',
  `request_time` datetime NOT NULL COMMENT '请求时间',
  `response_time` datetime NOT NULL COMMENT '响应时间',
  `execution_time` int(11) NOT NULL COMMENT '执行时间(ms)',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_app_id` (`app_id`),
  KEY `idx_api_path` (`api_path`),
  KEY `idx_request_time` (`request_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API调用日志表';
1.2 创建日志模型
代码语言:javascript代码运行次数:0运行复制
namespace app\model;

use think\Model;

class ApiCallLog extends Model
{
    protected $table = 'api_call_logs';
    
    protected $autoWriteTimestamp = 'datetime';
    
    protected $type = [
        'request_params' => 'json',
        'response_data' => 'json',
    ];
}
1.3 中间件实现日志记录
代码语言:javascript代码运行次数:0运行复制
namespace app\middleware;

use app\model\ApiCallLog;
use think\Request;
use think\Response;

class ApiLogger
{
    public function handle(Request $request, \Closure $next)
    {
        // 记录请求开始时间
        $startTime = microtime(true);
        
        // 继续执行请求
        $response = $next($request);
        
        // 记录日志
        $this->logRequest($request, $response, $startTime);
        
        return $response;
    }
    
    protected function logRequest(Request $request, Response $response, float $startTime)
    {
        try {
            $appId = $request->header('app-id', '');
            $executionTime = round((microtime(true) - $startTime) * 1000;
            
            ApiCallLog::create([
                'app_id' => $appId,
                'api_path' => $request->pathinfo(),
                'request_method' => $request->method(),
                'request_params' => $request->param(),
                'response_code' => $response->getCode(),
                'response_data' => $response->getData(),
                'ip_address' => $request->ip(),
                'user_agent' => $request->header('user-agent'),
                'request_time' => date('Y-m-d H:i:s', $startTime),
                'response_time' => date('Y-m-d H:i:s'),
                'execution_time' => $executionTime,
            ]);
        } catch (\Exception $e) {
            // 记录日志失败不应影响主流程
            \think\facade\Log::error('API日志记录失败: ' . $e->getMessage());
        }
    }
}
1.4 注册中间件

app/middleware.php中注册中间件:

代码语言:javascript代码运行次数:0运行复制
return [
    // 其他中间件...
    \app\middleware\ApiLogger::class,
];

二、请求频率限制的实现

2.1 使用Redis实现计数器

ThinkPHP6内置了缓存和Redis支持,我们可以利用Redis的高性能特性来实现频率限制。

2.1.1 频率限制配置

config/cache.php中配置Redis:

代码语言:javascript代码运行次数:0运行复制
'redis' => [
    'type'       => 'redis',
    'host'       => '127.0.0.1',
    'port'       => 6379,
    'password'   => '',
    'select'     => 0,
    'timeout'    => 0,
    'expire'     => 0,
    'persistent' => false,
    'prefix'     => 'api_limit:',
    'tag_prefix' => 'tag:',
],
2.2 频率限制中间件
代码语言:javascript代码运行次数:0运行复制
namespace app\middleware;

use think\Request;
use think\Response;
use think\facade\Cache;

class RateLimiter
{
    // 默认限制配置
    protected $defaultLimit = [
        'limit' => 100,          // 请求次数
        'window' => 60,          // 时间窗口(秒)
    ];
    
    public function handle(Request $request, \Closure $next, $config = null)
    {
        $appId = $request->header('app-id', 'anonymous');
        $ip = $request->ip();
        $apiPath = $request->pathinfo();
        
        // 获取限流配置
        $limitConfig = $this->getLimitConfig($appId, $apiPath, $config);
        
        // 生成唯一键
        $key = $this->generateKey($appId, $ip, $apiPath);
        
        // 检查是否超过限制
        if ($this->isRateLimited($key, $limitConfig)) {
            return $this->responseLimitExceeded($limitConfig);
        }
        
        return $next($request);
    }
    
    protected function getLimitConfig($appId, $apiPath, $config)
    {
        // 这里可以从数据库或配置文件中获取特定appId和apiPath的限流配置
        // 简化示例返回默认配置
        return $this->defaultLimit;
    }
    
    protected function generateKey($appId, $ip, $apiPath)
    {
        return md5("{$appId}_{$ip}_{$apiPath}");
    }
    
    protected function isRateLimited($key, $config)
    {
        $cache = Cache::store('redis');
        $now = time();
        
        // 使用Redis的有序集合实现滑动窗口计数
        $windowStart = $now - $config['window'];
        
        // 移除时间窗口外的记录
        $cache->zRemRangeByScore($key, 0, $windowStart);
        
        // 获取当前窗口内的请求数
        $requestCount = $cache->zCard($key);
        
        if ($requestCount >= $config['limit']) {
            return true;
        }
        
        // 添加当前请求时间戳
        $cache->zAdd($key, $now, $now);
        
        // 设置键的过期时间
        $cache->expire($key, $config['window']);
        
        return false;
    }
    
    protected function responseLimitExceeded($config)
    {
        $response = Response::create([
            'code' => 429,
            'message' => '请求过于频繁',
            'data' => [
                'limit' => $config['limit'],
                'window' => $config['window'],
            ]
        ], 'json', 429);
        
        $response->header([
            'X-RateLimit-Limit' => $config['limit'],
            'X-RateLimit-Remaining' => 0,
            'X-RateLimit-Reset' => time() + $config['window'],
        ]);
        
        return $response;
    }
}
2.3 应用频率限制中间件
2.3.1 全局中间件

app/middleware.php中注册全局中间件:

代码语言:javascript代码运行次数:0运行复制
return [
    // 其他中间件...
    \app\middleware\RateLimiter::class,
];
2.3.2 路由中间件

也可以在特定路由上应用不同的限制:

代码语言:javascript代码运行次数:0运行复制
Route::group('api', function() {
    Route::get('user/info', 'user/info')
        ->middleware(\app\middleware\RateLimiter::class, ['limit' => 50, 'window' => 60]);
    
    Route::post('user/update', 'user/update')
        ->middleware(\app\middleware\RateLimiter::class, ['limit' => 10, 'window' => 60]);
});

三、进阶优化

3.1 动态限流配置

将限流配置存储在数据库中,实现动态调整:

代码语言:javascript代码运行次数:0运行复制
protected function getLimitConfig($appId, $apiPath, $config)
{
    // 从缓存获取配置
    $cacheKey = "rate_limit_config:{$appId}:{$apiPath}";
    $config = Cache::get($cacheKey);
    
    if ($config) {
        return $config;
    }
    
    // 从数据库查询
    $config = \app\model\RateLimitConfig::where('app_id', $appId)
        ->where('api_path', $apiPath)
        ->find();
    
    if (!$config) {
        // 使用默认配置
        $config = $this->defaultLimit;
    }
    
    // 缓存配置
    Cache::set($cacheKey, $config, 3600);
    
    return $config;
}
3.2 分布式限流

对于分布式系统,可以使用Redis+Lua脚本实现原子操作:

代码语言:javascript代码运行次数:0运行复制
-- ratelimit.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

local windowStart = now - window
redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)

local current = redis.call('ZCARD', key)
if current >= limit then
    return 0
end

redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, window)
return 1

然后在PHP中调用:

代码语言:javascript代码运行次数:0运行复制
protected function isRateLimited($key, $config)
{
    $lua = file_get_contents(app_path().'lua/ratelimit.lua');
    $now = time();
    
    $result = Cache::store('redis')->eval($lua, [$key, $config['limit'], $config['window'], $now], 1);
    
    return $result == 0;
}
3.3 日志性能优化

对于高并发场景,日志记录可以改为异步处理:

代码语言:javascript代码运行次数:0运行复制
protected function logRequest(Request $request, Response $response, float $startTime)
{
    // 将日志数据放入队列
    \think\facade\Queue::push('app\job\ApiLogJob', [
        'app_id' => $request->header('app-id', ''),
        'api_path' => $request->pathinfo(),
        'request_method' => $request->method(),
        'request_params' => $request->param(),
        'response_code' => $response->getCode(),
        'response_data' => $response->getData(),
        'ip_address' => $request->ip(),
        'user_agent' => $request->header('user-agent'),
        'request_time' => date('Y-m-d H:i:s', $startTime),
        'response_time' => date('Y-m-d H:i:s'),
        'execution_time' => round((microtime(true) - $startTime) * 1000),
    ]);
}

四、总结

在ThinkPHP6中实现API调用日志和请求频率限制,我们可以:

  1. 通过中间件机制无侵入式地实现功能
  2. 使用Redis高效实现滑动窗口限流算法
  3. 采用异步处理提高日志记录性能
  4. 支持动态配置满足不同API和应用的限流需求

这些功能的实现不仅保护了API服务器的稳定性,还为后续的监控分析和计费提供了数据基础。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2025-04-24,如有侵权请联系 cloudcommunity@tencent 删除日志中间件apithinkphp6配置

ThinkPHP6 API开放平台:调用日志与请求频率限制的实现

在构建API开放平台时,调用日志记录和请求频率限制是两个至关重要的功能。调用日志帮助我们追踪API使用情况、排查问题,而频率限制则保护系统免受滥用和过载。本文将详细介绍如何在ThinkPHP6中实现这两大功能。

一、调用日志的实现

1.1 数据库设计

首先我们需要设计一个日志表来存储API调用记录:

代码语言:javascript代码运行次数:0运行复制
CREATE TABLE `api_call_logs` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `app_id` varchar(32) NOT NULL COMMENT '应用ID',
  `api_path` varchar(255) NOT NULL COMMENT 'API路径',
  `request_method` varchar(10) NOT NULL COMMENT '请求方法',
  `request_params` text COMMENT '请求参数',
  `response_code` int(11) NOT NULL COMMENT '响应状态码',
  `response_data` text COMMENT '响应数据',
  `ip_address` varchar(45) NOT NULL COMMENT 'IP地址',
  `user_agent` varchar(255) DEFAULT NULL COMMENT '用户代理',
  `request_time` datetime NOT NULL COMMENT '请求时间',
  `response_time` datetime NOT NULL COMMENT '响应时间',
  `execution_time` int(11) NOT NULL COMMENT '执行时间(ms)',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_app_id` (`app_id`),
  KEY `idx_api_path` (`api_path`),
  KEY `idx_request_time` (`request_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API调用日志表';
1.2 创建日志模型
代码语言:javascript代码运行次数:0运行复制
namespace app\model;

use think\Model;

class ApiCallLog extends Model
{
    protected $table = 'api_call_logs';
    
    protected $autoWriteTimestamp = 'datetime';
    
    protected $type = [
        'request_params' => 'json',
        'response_data' => 'json',
    ];
}
1.3 中间件实现日志记录
代码语言:javascript代码运行次数:0运行复制
namespace app\middleware;

use app\model\ApiCallLog;
use think\Request;
use think\Response;

class ApiLogger
{
    public function handle(Request $request, \Closure $next)
    {
        // 记录请求开始时间
        $startTime = microtime(true);
        
        // 继续执行请求
        $response = $next($request);
        
        // 记录日志
        $this->logRequest($request, $response, $startTime);
        
        return $response;
    }
    
    protected function logRequest(Request $request, Response $response, float $startTime)
    {
        try {
            $appId = $request->header('app-id', '');
            $executionTime = round((microtime(true) - $startTime) * 1000;
            
            ApiCallLog::create([
                'app_id' => $appId,
                'api_path' => $request->pathinfo(),
                'request_method' => $request->method(),
                'request_params' => $request->param(),
                'response_code' => $response->getCode(),
                'response_data' => $response->getData(),
                'ip_address' => $request->ip(),
                'user_agent' => $request->header('user-agent'),
                'request_time' => date('Y-m-d H:i:s', $startTime),
                'response_time' => date('Y-m-d H:i:s'),
                'execution_time' => $executionTime,
            ]);
        } catch (\Exception $e) {
            // 记录日志失败不应影响主流程
            \think\facade\Log::error('API日志记录失败: ' . $e->getMessage());
        }
    }
}
1.4 注册中间件

app/middleware.php中注册中间件:

代码语言:javascript代码运行次数:0运行复制
return [
    // 其他中间件...
    \app\middleware\ApiLogger::class,
];

二、请求频率限制的实现

2.1 使用Redis实现计数器

ThinkPHP6内置了缓存和Redis支持,我们可以利用Redis的高性能特性来实现频率限制。

2.1.1 频率限制配置

config/cache.php中配置Redis:

代码语言:javascript代码运行次数:0运行复制
'redis' => [
    'type'       => 'redis',
    'host'       => '127.0.0.1',
    'port'       => 6379,
    'password'   => '',
    'select'     => 0,
    'timeout'    => 0,
    'expire'     => 0,
    'persistent' => false,
    'prefix'     => 'api_limit:',
    'tag_prefix' => 'tag:',
],
2.2 频率限制中间件
代码语言:javascript代码运行次数:0运行复制
namespace app\middleware;

use think\Request;
use think\Response;
use think\facade\Cache;

class RateLimiter
{
    // 默认限制配置
    protected $defaultLimit = [
        'limit' => 100,          // 请求次数
        'window' => 60,          // 时间窗口(秒)
    ];
    
    public function handle(Request $request, \Closure $next, $config = null)
    {
        $appId = $request->header('app-id', 'anonymous');
        $ip = $request->ip();
        $apiPath = $request->pathinfo();
        
        // 获取限流配置
        $limitConfig = $this->getLimitConfig($appId, $apiPath, $config);
        
        // 生成唯一键
        $key = $this->generateKey($appId, $ip, $apiPath);
        
        // 检查是否超过限制
        if ($this->isRateLimited($key, $limitConfig)) {
            return $this->responseLimitExceeded($limitConfig);
        }
        
        return $next($request);
    }
    
    protected function getLimitConfig($appId, $apiPath, $config)
    {
        // 这里可以从数据库或配置文件中获取特定appId和apiPath的限流配置
        // 简化示例返回默认配置
        return $this->defaultLimit;
    }
    
    protected function generateKey($appId, $ip, $apiPath)
    {
        return md5("{$appId}_{$ip}_{$apiPath}");
    }
    
    protected function isRateLimited($key, $config)
    {
        $cache = Cache::store('redis');
        $now = time();
        
        // 使用Redis的有序集合实现滑动窗口计数
        $windowStart = $now - $config['window'];
        
        // 移除时间窗口外的记录
        $cache->zRemRangeByScore($key, 0, $windowStart);
        
        // 获取当前窗口内的请求数
        $requestCount = $cache->zCard($key);
        
        if ($requestCount >= $config['limit']) {
            return true;
        }
        
        // 添加当前请求时间戳
        $cache->zAdd($key, $now, $now);
        
        // 设置键的过期时间
        $cache->expire($key, $config['window']);
        
        return false;
    }
    
    protected function responseLimitExceeded($config)
    {
        $response = Response::create([
            'code' => 429,
            'message' => '请求过于频繁',
            'data' => [
                'limit' => $config['limit'],
                'window' => $config['window'],
            ]
        ], 'json', 429);
        
        $response->header([
            'X-RateLimit-Limit' => $config['limit'],
            'X-RateLimit-Remaining' => 0,
            'X-RateLimit-Reset' => time() + $config['window'],
        ]);
        
        return $response;
    }
}
2.3 应用频率限制中间件
2.3.1 全局中间件

app/middleware.php中注册全局中间件:

代码语言:javascript代码运行次数:0运行复制
return [
    // 其他中间件...
    \app\middleware\RateLimiter::class,
];
2.3.2 路由中间件

也可以在特定路由上应用不同的限制:

代码语言:javascript代码运行次数:0运行复制
Route::group('api', function() {
    Route::get('user/info', 'user/info')
        ->middleware(\app\middleware\RateLimiter::class, ['limit' => 50, 'window' => 60]);
    
    Route::post('user/update', 'user/update')
        ->middleware(\app\middleware\RateLimiter::class, ['limit' => 10, 'window' => 60]);
});

三、进阶优化

3.1 动态限流配置

将限流配置存储在数据库中,实现动态调整:

代码语言:javascript代码运行次数:0运行复制
protected function getLimitConfig($appId, $apiPath, $config)
{
    // 从缓存获取配置
    $cacheKey = "rate_limit_config:{$appId}:{$apiPath}";
    $config = Cache::get($cacheKey);
    
    if ($config) {
        return $config;
    }
    
    // 从数据库查询
    $config = \app\model\RateLimitConfig::where('app_id', $appId)
        ->where('api_path', $apiPath)
        ->find();
    
    if (!$config) {
        // 使用默认配置
        $config = $this->defaultLimit;
    }
    
    // 缓存配置
    Cache::set($cacheKey, $config, 3600);
    
    return $config;
}
3.2 分布式限流

对于分布式系统,可以使用Redis+Lua脚本实现原子操作:

代码语言:javascript代码运行次数:0运行复制
-- ratelimit.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

local windowStart = now - window
redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)

local current = redis.call('ZCARD', key)
if current >= limit then
    return 0
end

redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, window)
return 1

然后在PHP中调用:

代码语言:javascript代码运行次数:0运行复制
protected function isRateLimited($key, $config)
{
    $lua = file_get_contents(app_path().'lua/ratelimit.lua');
    $now = time();
    
    $result = Cache::store('redis')->eval($lua, [$key, $config['limit'], $config['window'], $now], 1);
    
    return $result == 0;
}
3.3 日志性能优化

对于高并发场景,日志记录可以改为异步处理:

代码语言:javascript代码运行次数:0运行复制
protected function logRequest(Request $request, Response $response, float $startTime)
{
    // 将日志数据放入队列
    \think\facade\Queue::push('app\job\ApiLogJob', [
        'app_id' => $request->header('app-id', ''),
        'api_path' => $request->pathinfo(),
        'request_method' => $request->method(),
        'request_params' => $request->param(),
        'response_code' => $response->getCode(),
        'response_data' => $response->getData(),
        'ip_address' => $request->ip(),
        'user_agent' => $request->header('user-agent'),
        'request_time' => date('Y-m-d H:i:s', $startTime),
        'response_time' => date('Y-m-d H:i:s'),
        'execution_time' => round((microtime(true) - $startTime) * 1000),
    ]);
}

四、总结

在ThinkPHP6中实现API调用日志和请求频率限制,我们可以:

  1. 通过中间件机制无侵入式地实现功能
  2. 使用Redis高效实现滑动窗口限流算法
  3. 采用异步处理提高日志记录性能
  4. 支持动态配置满足不同API和应用的限流需求

这些功能的实现不仅保护了API服务器的稳定性,还为后续的监控分析和计费提供了数据基础。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2025-04-24,如有侵权请联系 cloudcommunity@tencent 删除日志中间件apithinkphp6配置

本文标签: ThinkPHP6 API开放平台调用日志与请求频率限制的实现