中间件系统
CondorAdmin 使用 Webman 中间件机制实现跨切面功能,包括认证、权限、日志、跨域等。中间件按照定义顺序串联执行,形成责任链模式。
中间件概述
执行流程
HTTP 请求
↓
CrossDomain(跨域处理)
↓
AuthToken(Token 认证)
↓
AuthPermission(权限验证)
↓
Lang(多语言处理)
↓
OperateLog(操作日志)
↓
Controller(业务逻辑)
↓
Response(响应返回)中间件接口
所有中间件必须实现 MiddlewareInterface 接口:
interface MiddlewareInterface
{
public function process(Request $request, callable $handler): Response;
}参数说明:
$request:当前 HTTP 请求对象$handler:下一个中间件/控制器的调用句柄- 返回值:
Response对象
核心中间件
1. CrossDomain(跨域中间件)
作用:处理跨域请求,允许前端访问后端 API。
位置:plugin/condoradmin/app/middleware/CrossDomain.php
实现:
<?php
namespace plugin\condoradmin\app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class CrossDomain implements MiddlewareInterface
{
public function process(Request $request, callable $handler): Response
{
// 预检请求(OPTIONS)直接返回
if ($request->method() === 'OPTIONS') {
return response('', 200, [
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,OPTIONS',
'Access-Control-Allow-Headers' => 'Content-Type,Authorization,X-Requested-With',
'Access-Control-Max-Age' => '86400',
]);
}
// 正常请求继续处理
$response = $handler($request);
// 添加跨域响应头
$response->withHeaders([
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Credentials' => 'true',
]);
return $response;
}
}配置:
生产环境建议限制来源域名:
// 仅允许特定域名
'Access-Control-Allow-Origin' => 'https://admin.yourdomain.com'2. AuthToken(Token 认证中间件)
作用:验证请求中的 Token 是否有效,识别用户身份。
位置:plugin/condoradmin/app/middleware/AuthToken.php
核心逻辑:
public function process(Request $request, callable $handler): Response
{
$controller = $request->controller;
$action = $request->action;
// 无控制器信息,跳过
if (!$controller) {
return $handler($request);
}
// 获取控制器的 $noNeedLogin 配置
$class = new \ReflectionClass($controller);
$properties = $class->getDefaultProperties();
$noNeedLogin = $properties['noNeedLogin'] ?? [];
// 不需要登录的方法,直接放行
if (in_array($action, $noNeedLogin)) {
return $handler($request);
}
// 获取 Token 信息
$tokenInfo = getCurrentInfo();
if ($tokenInfo === false) {
return json([
'code' => 401,
'msg' => trans('common.please.log.in.first'),
'data' => []
]);
}
// 验证应用标识
$expectedApp = config('plugin.condoradmin.condor.auth_token_expected_app', 'condoradmin');
if (!isset($tokenInfo['app']) || $expectedApp !== $tokenInfo['app']) {
return json(['code' => 403, 'msg' => trans('common.access.denied'), 'data' => []]);
}
// 验证 Token 是否在 Redis 中有效
$token = str_replace('Bearer ', '', $request->header('authorization'));
$adminId = Redis::get('admin:token:' . $token);
if (!$adminId) {
return json(['code' => 401, 'msg' => trans('common.invalid.token'), 'data' => []]);
}
return $handler($request);
}Token 存储结构:
Redis Key: admin:token:{token}
Value: {admin_id}
TTL: 7200 秒(2小时)控制器配置(免登录方法):
class AccountController extends Backend
{
protected $noNeedLogin = ['login', 'captcha', 'publicKey'];
}3. AuthPermission(权限验证中间件)
作用:验证用户是否有权限访问当前接口。
位置:plugin/condoradmin/app/middleware/AuthPermission.php
核心逻辑:
public function process(Request $request, callable $handler): Response
{
$controller = $request->controller;
$action = $request->action;
if (!$controller) {
return $handler($request);
}
// 获取控制器鉴权配置
$class = new \ReflectionClass($controller);
$properties = $class->getDefaultProperties();
$noNeedLogin = $properties['noNeedLogin'] ?? [];
if (in_array($action, $noNeedLogin)) {
return $handler($request);
}
// 初始化 Auth 对象
$auth = new \plugin\condoradmin\app\library\Auth();
$auth->initUser();
// 不需要权限验证的方法
$noNeedRight = $properties['noNeedRight'] ?? [];
if (in_array($action, $noNeedRight)) {
Context::set('auth', $auth);
return $handler($request);
}
// 检查权限
if ($auth->check($request->path(), $auth->id) !== true) {
return json([
'code' => 403,
'msg' => trans('common.access.denied'),
'data' => null
]);
}
// 将 Auth 对象存入 Context,供控制器使用
Context::set('auth', $auth);
return $handler($request);
}权限检查流程:
- 从请求路径提取权限标识(如
/api/condoradmin/admin/index→admin:index) - 查询用户的权限列表(从 Redis 缓存或数据库)
- 判断权限列表中是否包含该权限
- 超级管理员(
'*'权限)直接放行
控制器配置(免权限方法):
class AdminController extends Backend
{
// 需要登录但不需要权限验证
protected $noNeedRight = ['profile', 'updatePassword'];
}4. Lang(多语言中间件)
作用:根据请求头设置当前语言环境。
位置:plugin/condoradmin/app/middleware/Lang.php
实现:
public function process(Request $request, callable $handler): Response
{
$lang = $request->header('accept-language', 'zh-cn');
// 标准化语言代码
$lang = strtolower(str_replace('_', '-', $lang));
// 支持的语言列表
$supportedLangs = ['zh-cn', 'en'];
// 如果不在支持列表中,使用默认语言
if (!in_array($lang, $supportedLangs)) {
$lang = 'zh-cn';
}
// 设置当前语言
locale($lang);
return $handler($request);
}请求示例:
GET /api/condoradmin/admin/index
Accept-Language: zh-CN语言文件路径:
plugin/condoradmin/app/i18n/
├── zh-cn.php # 简体中文
└── en.php # 英文使用翻译:
// 控制器中
return $this->fail(trans('condoradmin.parameter.can.not.be.empty'));
// 翻译文件(zh-cn.php)
return [
'parameter.can.not.be.empty' => '参数不能为空',
];5. OperateLog(操作日志中间件)
作用:自动记录用户的操作日志(请求路径、参数、响应等)。
位置:plugin/condoradmin/app/middleware/OperateLog.php
实现:
public function process(Request $request, callable $handler): Response
{
// 执行请求
$response = $handler($request);
// 异步记录日志(不阻塞响应)
defer(function () use ($request, $response) {
$auth = Context::get('auth');
// 仅记录已登录用户的操作
if (!$auth || !$auth->id) {
return;
}
// 构建日志数据
$log = [
'admin_id' => $auth->id,
'username' => $auth->username,
'method' => $request->method(),
'path' => $request->path(),
'ip' => $request->getRealIp(),
'params' => json_encode($request->all(), JSON_UNESCAPED_UNICODE),
'user_agent' => $request->header('user-agent'),
'response_code' => $response->getStatusCode(),
'created_at' => date('Y-m-d H:i:s'),
];
// 写入数据库
Db::table('system_admin_log')->insert($log);
});
return $response;
}日志表结构:
CREATE TABLE `system_admin_log` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`admin_id` int unsigned NOT NULL COMMENT '管理员ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`method` varchar(10) NOT NULL COMMENT '请求方法',
`path` varchar(255) NOT NULL COMMENT '请求路径',
`ip` varchar(50) NOT NULL COMMENT 'IP地址',
`params` text COMMENT '请求参数',
`user_agent` varchar(500) DEFAULT NULL COMMENT 'User-Agent',
`response_code` int DEFAULT NULL COMMENT '响应状态码',
`created_at` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `admin_id` (`admin_id`),
KEY `created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;查询日志:
// 获取某个用户的操作日志
$logs = Db::table('system_admin_log')
->where('admin_id', 1)
->orderBy('created_at', 'desc')
->limit(100)
->get();中间件配置
全局中间件
在 config/middleware.php 中配置:
return [
'' => [
// 全局中间件(所有请求都经过)
plugin\condoradmin\app\middleware\CrossDomain::class,
]
];路由中间件
在路由定义时指定中间件:
// config/route.php
use Webman\Route;
use plugin\condoradmin\app\middleware\AuthToken;
use plugin\condoradmin\app\middleware\AuthPermission;
Route::group('/api/condoradmin', function () {
Route::any('/admin/[{action}]', [AdminController::class, 'handle']);
})->middleware([
AuthToken::class,
AuthPermission::class,
Lang::class,
OperateLog::class,
]);控制器中间件
在控制器中配置(通过 $noNeedLogin 和 $noNeedRight):
class AdminController extends Backend
{
// 无需登录的方法
protected $noNeedLogin = ['login', 'captcha'];
// 无需权限验证的方法(但需要登录)
protected $noNeedRight = ['profile', 'updatePassword'];
}自定义中间件
创建中间件
<?php
namespace plugin\yourplugin\app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class RateLimitMiddleware implements MiddlewareInterface
{
public function process(Request $request, callable $handler): Response
{
// 获取用户标识(IP 或用户ID)
$key = 'rate_limit:' . $request->getRealIp();
// Redis 计数
$count = Redis::incr($key);
if ($count === 1) {
// 首次请求,设置过期时间(60秒)
Redis::expire($key, 60);
}
if ($count > 60) {
// 超过限制,返回 429
return json([
'code' => 429,
'msg' => '请求过于频繁,请稍后再试',
'data' => []
]);
}
// 继续处理
$response = $handler($request);
// 添加限流响应头
$response->withHeaders([
'X-RateLimit-Limit' => '60',
'X-RateLimit-Remaining' => (60 - $count),
]);
return $response;
}
}注册中间件
// config/route.php
Route::group('/api', function () {
// 路由定义
})->middleware([
\plugin\yourplugin\app\middleware\RateLimitMiddleware::class,
]);中间件执行顺序
请求阶段(Before)
1. CrossDomain # 跨域处理
2. AuthToken # Token 验证
3. AuthPermission # 权限验证
4. Lang # 语言设置
5. RateLimit (可选) # 限流
6. Controller # 执行业务逻辑响应阶段(After)
6. Controller # 返回响应
5. OperateLog # 记录日志
4. Lang # (无操作)
3. AuthPermission # (无操作)
2. AuthToken # (无操作)
1. CrossDomain # 添加响应头注意:中间件的 return $handler($request) 之后的代码会在响应返回时执行(类似 AOP 的 AfterReturning)。
中间件最佳实践
1. 职责单一
每个中间件只负责一个功能:
- ✅ 好:
AuthToken只验证 Token - ❌ 坏:
AuthToken中同时验证 Token 和权限
2. 性能优化
- 缓存:频繁读取的数据使用 Redis 缓存
- 异步:日志记录使用
defer()异步执行 - 提前返回:不满足条件时立即返回,避免不必要的处理
// 提前返回
if (in_array($action, $noNeedLogin)) {
return $handler($request);
}3. 错误处理
中间件中的异常会中断请求,返回统一错误响应:
try {
$auth = new Auth();
$auth->initUser();
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => config('app.debug') ? $e->getMessage() : '系统错误',
'data' => []
]);
}4. Context 使用
使用 Webman 的 Context 在中间件和控制器之间传递数据:
// 中间件中设置
Context::set('auth', $auth);
Context::set('request_id', uniqid());
// 控制器中获取
$auth = Context::get('auth');
$requestId = Context::get('request_id');常见问题
Q1: 中间件执行顺序如何控制?
中间件按照配置文件或路由定义中的顺序执行。
Q2: 如何在控制器中跳过中间件?
通过 $noNeedLogin 和 $noNeedRight 配置:
protected $noNeedLogin = ['publicMethod'];
protected $noNeedRight = ['profileMethod'];Q3: 如何调试中间件?
// 在中间件中打印日志
Log::info('Middleware: ' . static::class, [
'path' => $request->path(),
'method' => $request->method(),
]);Q4: 中间件异常如何处理?
Webman 会自动捕获异常并返回 500 错误。建议在中间件中使用 try-catch 返回友好的错误提示。
相关文档
- 📖 整体架构
- 🔐 权限系统
- 📦 Backend 控制器