Skip to content

中间件系统

CondorAdmin 使用 Webman 中间件机制实现跨切面功能,包括认证、权限、日志、跨域等。中间件按照定义顺序串联执行,形成责任链模式。

中间件概述

执行流程

HTTP 请求

CrossDomain(跨域处理)

AuthToken(Token 认证)

AuthPermission(权限验证)

Lang(多语言处理)

OperateLog(操作日志)

Controller(业务逻辑)

Response(响应返回)

中间件接口

所有中间件必须实现 MiddlewareInterface 接口:

php
interface MiddlewareInterface
{
    public function process(Request $request, callable $handler): Response;
}

参数说明

  • $request:当前 HTTP 请求对象
  • $handler:下一个中间件/控制器的调用句柄
  • 返回值Response 对象

核心中间件

1. CrossDomain(跨域中间件)

作用:处理跨域请求,允许前端访问后端 API。

位置plugin/condoradmin/app/middleware/CrossDomain.php

实现

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;
    }
}

配置

生产环境建议限制来源域名:

php
// 仅允许特定域名
'Access-Control-Allow-Origin' => 'https://admin.yourdomain.com'

2. AuthToken(Token 认证中间件)

作用:验证请求中的 Token 是否有效,识别用户身份。

位置plugin/condoradmin/app/middleware/AuthToken.php

核心逻辑

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小时)

控制器配置(免登录方法):

php
class AccountController extends Backend
{
    protected $noNeedLogin = ['login', 'captcha', 'publicKey'];
}

3. AuthPermission(权限验证中间件)

作用:验证用户是否有权限访问当前接口。

位置plugin/condoradmin/app/middleware/AuthPermission.php

核心逻辑

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);
}

权限检查流程

  1. 从请求路径提取权限标识(如 /api/condoradmin/admin/indexadmin:index
  2. 查询用户的权限列表(从 Redis 缓存或数据库)
  3. 判断权限列表中是否包含该权限
  4. 超级管理员('*' 权限)直接放行

控制器配置(免权限方法):

php
class AdminController extends Backend
{
    // 需要登录但不需要权限验证
    protected $noNeedRight = ['profile', 'updatePassword'];
}

4. Lang(多语言中间件)

作用:根据请求头设置当前语言环境。

位置plugin/condoradmin/app/middleware/Lang.php

实现

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);
}

请求示例

http
GET /api/condoradmin/admin/index
Accept-Language: zh-CN

语言文件路径

plugin/condoradmin/app/i18n/
├── zh-cn.php  # 简体中文
└── en.php     # 英文

使用翻译

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

实现

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;
}

日志表结构

sql
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;

查询日志

php
// 获取某个用户的操作日志
$logs = Db::table('system_admin_log')
    ->where('admin_id', 1)
    ->orderBy('created_at', 'desc')
    ->limit(100)
    ->get();

中间件配置

全局中间件

config/middleware.php 中配置:

php
return [
    '' => [
        // 全局中间件(所有请求都经过)
        plugin\condoradmin\app\middleware\CrossDomain::class,
    ]
];

路由中间件

在路由定义时指定中间件:

php
// 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):

php
class AdminController extends Backend
{
    // 无需登录的方法
    protected $noNeedLogin = ['login', 'captcha'];
    
    // 无需权限验证的方法(但需要登录)
    protected $noNeedRight = ['profile', 'updatePassword'];
}

自定义中间件

创建中间件

php
<?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;
    }
}

注册中间件

php
// 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() 异步执行
  • 提前返回:不满足条件时立即返回,避免不必要的处理
php
// 提前返回
if (in_array($action, $noNeedLogin)) {
    return $handler($request);
}

3. 错误处理

中间件中的异常会中断请求,返回统一错误响应:

php
try {
    $auth = new Auth();
    $auth->initUser();
} catch (\Exception $e) {
    return json([
        'code' => 500,
        'msg' => config('app.debug') ? $e->getMessage() : '系统错误',
        'data' => []
    ]);
}

4. Context 使用

使用 Webman 的 Context 在中间件和控制器之间传递数据:

php
// 中间件中设置
Context::set('auth', $auth);
Context::set('request_id', uniqid());

// 控制器中获取
$auth = Context::get('auth');
$requestId = Context::get('request_id');

常见问题

Q1: 中间件执行顺序如何控制?

中间件按照配置文件或路由定义中的顺序执行。

Q2: 如何在控制器中跳过中间件?

通过 $noNeedLogin$noNeedRight 配置:

php
protected $noNeedLogin = ['publicMethod'];
protected $noNeedRight = ['profileMethod'];

Q3: 如何调试中间件?

php
// 在中间件中打印日志
Log::info('Middleware: ' . static::class, [
    'path' => $request->path(),
    'method' => $request->method(),
]);

Q4: 中间件异常如何处理?

Webman 会自动捕获异常并返回 500 错误。建议在中间件中使用 try-catch 返回友好的错误提示。


相关文档