帮助文档
{{userInfo.nickname}}
用户设置 退出登录

{{wikiTitle}}

中间件使用说明

中间件使用说明

概述

CRMEB多店系统使用ThinkPHP框架的中间件机制来处理各种请求过滤、权限验证、日志记录等功能。中间件在请求到达控制器之前或响应返回给客户端之后执行特定的逻辑处理。

目录结构

app/http/middleware/
├── AllowOriginMiddleware.php      # 跨域处理中间件
├── BlockerMiddleware.php          # Redis锁防重复提交中间件
├── InstallMiddleware.php          # 安装检测中间件
├── StationOpenMiddleware.php      # 站点开放状态中间件
├── SystemLogMiddleware.php        # 系统日志中间件
├── admin/                        # 后台管理中间件
│   ├── AdminAuthTokenMiddleware.php  # 后台登录验证中间件
│   └── AdminCkeckRoleMiddleware.php  # 后台权限验证中间件
├── api/                          # API中间件
│   ├── AuthTokenMiddleware.php       # 用户登录验证中间件
│   ├── ClientMiddleware.php          # 客户身份验证中间件
│   ├── CommunityOpenMiddleware.php   # 社区开放中间件
│   ├── CustomerMiddleware.php        # 客服中间件
│   └── RateLimiterMiddleware.php     # 限流中间件
├── cashier/                      # 收银台中间件
├── kefu/                         # 客服中间件
├── out/                          # 外部接口中间件
├── store/                        # 门店中间件
└── supplier/                     # 供应商中间件

中间件接口定义

所有中间件都实现了 crmeb\interfaces\MiddlewareInterface 接口:

interface MiddlewareInterface
{
    public function handle($request, \Closure $next);
}

核心中间件详解

1. 认证中间件

1.1 后台登录验证中间件 (AdminAuthTokenMiddleware)

<?php
namespace app\http\middleware\admin;

use app\Request;
use app\services\system\admin\AdminAuthServices;
use crmeb\interfaces\MiddlewareInterface;
use think\facade\Config;

/**
 * 后台登录验证中间件
 */
class AdminAuthTokenMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, \Closure $next)
    {
        $authInfo = null;
        // 从请求头获取token
        $token = trim(ltrim($request->header(Config::get('cookie.token_name', 'Authori-zation')), 'Bearer'));

        /** @var AdminAuthServices $service */
        $service = app()->make(AdminAuthServices::class);
        $adminInfo = $service->parseToken($token);

        // 设置请求对象的登录信息
        $request->isAdminLogin = !is_null($adminInfo);
        $request->adminId = (int)$adminInfo['id'];
        $request->adminInfo = $adminInfo;
        $request->adminType = $adminInfo['admin_type'] ?? 0;
        $request->agentId = $request->adminType == 3 ? ($adminInfo['relation_id'] ?? 0) : 0;

        return $next($request);
    }
}

功能说明:

  • 解析JWT token并验证登录状态
  • 将用户信息注入到请求对象中
  • 支持不同类型的管理员(普通管理员、代理商等)

1.2 用户登录验证中间件 (AuthTokenMiddleware)

<?php
namespace app\http\middleware\api;

use app\Request;
use app\services\user\UserAuthServices;
use crmeb\exceptions\AuthException;
use crmeb\interfaces\MiddlewareInterface;

class AuthTokenMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, \Closure $next, bool $force = true)
    {
        $authInfo = null;
        // 从请求头获取token
        $token = trim(ltrim($request->header('Authori-zation'), 'Bearer'));
        if (!$token) $token = trim(ltrim($request->header('Authorization'), 'Bearer'));

        try {
            /** @var UserAuthServices $service */
            $service = app()->make(UserAuthServices::class);
            $authInfo = $service->parseToken($token);
        } catch (AuthException $e) {
            if ($force)
                return app('json')->make($e->getCode(), $e->getMessage());
        }

        if (!is_null($authInfo)) {
            // 设置用户信息获取方法
            $request->user = function (string $key = null) use (&$authInfo) {
                if ($key) {
                    return $authInfo['user'][$key] ?? '';
                }
                return $authInfo['user'];
            };
            $request->tokenData = $authInfo['tokenData'];
        }

        // 设置登录状态和用户ID
        $request->isLogin = !is_null($authInfo);
        $request->uid = is_null($authInfo) ? 0 : (int)$authInfo['user']->uid;

        // 社区用户写入
        if ($request->uid) {
            /** @var CommunityUserServices $communityUserServices */
            $communityUserServices = app()->make(CommunityUserServices::class);
            $communityUserServices->hasUser($request->uid);
        }

        return $next($request);
    }
}

功能说明:

  • 验证用户JWT token
  • 支持可选强制验证(force参数)
  • 提供便捷的用户信息获取方法
  • 处理社区用户关联

1.3 权限验证中间件 (AdminCkeckRoleMiddleware)

<?php
namespace app\http\middleware\admin;

use app\Request;
use app\services\system\SystemRoleServices;
use crmeb\exceptions\AuthException;
use crmeb\interfaces\MiddlewareInterface;
use crmeb\utils\ApiErrorCode;

/**
 * 权限规则验证
 */
class AdminCkeckRoleMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, \Closure $next)
    {
        if (!$request->adminId() || !$request->adminInfo())
            throw new AuthException(ApiErrorCode::ERR_ADMINID_VOID);

        // 检查是否需要权限验证
        if ($request->adminInfo()['level'] || $request->adminType() == 3) {
            /** @var SystemRoleServices $systemRoleService */
            $systemRoleService = app()->make(SystemRoleServices::class);
            $systemRoleService->verifiAuth($request);
        }

        return $next($request);
    }
}

功能说明:

  • 验证管理员是否已登录
  • 根据用户级别判断是否需要权限验证
  • 调用权限服务验证具体权限

2. 安全中间件

2.1 Redis锁防重复提交中间件 (BlockerMiddleware)

<?php
namespace app\http\middleware;

use app\Request;
use crmeb\exceptions\ApiException;
use crmeb\interfaces\MiddlewareInterface;
use crmeb\services\CacheService;

/**
 * Redis锁防重复提交中间件
 */
class BlockerMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, \Closure $next, string $type = 'user')
    {
        $id = 0;
        switch ($type) {
            case 'user': // 移动端
                $id = $request->uid();
                break;
            case 'cashier': // 收银台
                $id = $request->cashierId();
                break;
        }

        // 生成唯一键
        $key = md5($request->rule()->getRule() . $id . json_encode($request->param()));

        if (!CacheService::setMutex($key)) {
            throw new ApiException('操作太频繁,请稍后再试');
        }

        $response = $next($request);

        $this->after($response, $key);

        return $response;
    }

    public function after($response, $key)
    {
        CacheService::delMutex($key);
    }
}

功能说明:

  • 防止重复提交
  • 使用Redis分布式锁
  • 支持不同用户类型
  • 请求完成后自动释放锁

2.2 限流中间件 (RateLimiterMiddleware)

<?php
namespace app\http\middleware\api;

use app\Request;
use think\exception\ValidateException;
use think\facade\Cache;

/**
 * 限流中间件
 */
class RateLimiterMiddleware
{
    public function handle(Request $request, \Closure $next)
    {
        $option = $request->rule()->getOption();

        // 是否开启限流
        if (isset($option['rateLimiter']) && $option['rateLimiter']) {
            $rule = trim(strtolower($request->rule()->getRule()));
            $uid = 0;

            if ($request->hasMacro('uid') && !empty($option['isUser'])) {
                $uid = $request->uid();
            }

            $key = md5($rule . $uid);

            $this->createLimit($key, $option['limitNum'], $option['expire']);
        }

        return $next($request);
    }

    /**
     * 创建限流
     */
    protected function createLimit(string $key, int $initNum, int $expire)
    {
        $nowTime = time();

        /** @var \Redis $redis */
        $redis = Cache::store('redis')->handler();

        $script = <<<LUA
local key = KEYS[1]
local initNum = tonumber(ARGV[1])
local expire = tonumber(ARGV[2])
local nowTime = tonumber(ARGV[3])

local limitVal = redis.call('get', key)

if limitVal then
    limitVal = cjson.decode(limitVal)
    local newNum = math.min(initNum, (limitVal.num - 1) + ((initNum / expire) * (nowTime - limitVal.time)))
    if newNum <= 0 then
        return 0
    else
        local redisVal = {num = newNum, time = nowTime}
        redis.call('set', key, cjson.encode(redisVal))
        redis.call('expire', key, expire)
        return 1
    end
else
    local redisVal = {num = initNum, time = nowTime}
    redis.call('set', key, cjson.encode(redisVal))
    redis.call('expire', key, expire)
    return 1
end
LUA;

        $result = $redis->eval($script, [$key, $initNum, $expire, $nowTime], 1);
        if (!$result) {
            throw new ValidateException('访问频次过多!');
        }

        return true;
    }
}

功能说明:

  • 基于Redis Lua脚本的滑动窗口限流
  • 支持按用户维度限流
  • 可配置限流参数
  • 使用Lua脚本保证原子性

3. 功能中间件

3.1 跨域处理中间件 (AllowOriginMiddleware)

<?php
namespace app\http\middleware;

use app\Request;
use crmeb\interfaces\MiddlewareInterface;
use think\facade\Config;
use think\Response;

/**
 * 跨域中间件
 */
class AllowOriginMiddleware implements MiddlewareInterface
{
    protected $cookieDomain;

    public function handle(Request $request, \Closure $next)
    {
        $this->cookieDomain = Config::get('cookie.domain', '');
        $header = Config::get('cookie.header');
        $origin = $request->header('origin');

        if ($origin && ('' == $this->cookieDomain || strpos($origin, $this->cookieDomain)))
            $header['Access-Control-Allow-Origin'] = $origin;

        if ($request->method(true) == 'OPTIONS') {
            $response = Response::create('ok')->code(200)->header($header);
        } else {
            $response = $next($request)->header($header);
        }

        $request->filter(['strip_tags', 'addslashes', 'trim']);
        return $response;
    }
}

功能说明:

  • 处理CORS跨域请求
  • 支持预检请求(OPTIONS)
  • 安全的域名白名单验证

3.2 系统日志中间件 (SystemLogMiddleware)

<?php
namespace app\http\middleware;

use app\Request;
use app\jobs\system\AdminLogJob;
use crmeb\interfaces\MiddlewareInterface;

/**
 * 日志中间件
 */
class SystemLogMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, \Closure $next, string $type = 'admin')
    {
        $method = trim(strtolower($request->method()));
        $rule = trim(strtolower($request->rule()->getRule()));
        $id = 0;
        $account = '未知/未登录';

        switch ($type) {
            case 'user': // 移动端
                $id = $request->hasMacro('uid') ? (int)$request->uid() : 0;
                $account = $request->user()['nickname'] ?? '未知/未登录';
                break;
            case 'admin': // 平台
                $id = $request->hasMacro('adminId') ? (int)$request->adminId() : 0;
                $account = $request->adminInfo()['account'] ?? '';
                break;
            case 'store': // 门店
                $id = $request->hasMacro('storeStaffId') ? (int)$request->storeStaffId() : 0;
                $account = $request->storeStaffInfo()['account'] ?? '未知/未登录';
                break;
            case 'cashier': // 收银台
                $id = $request->hasMacro('cashierId') ? (int)$request->cashierId() : 0;
                $account = $request->cashierInfo()['account'] ?? '未知/未登录';
                break;
            case 'supplier': // 供应商
                $id = $request->hasMacro('supplierId') ? (int)$request->supplierId() : 0;
                $account = $request->supplierInfo()['supplier_name'] ?? '未知/未登录';
                break;
        }

        // 记录后台日志
        AdminLogJob::dispatch([$id, $account, $method, $rule, $request->ip(), $type]);

        return $next($request);
    }
}

功能说明:

  • 记录用户操作日志
  • 支持多种用户类型(用户、后台、门店、收银台、供应商)
  • 使用队列异步记录日志

3.3 站点状态中间件 (StationOpenMiddleware)

<?php
namespace app\http\middleware;

use app\Request;
use crmeb\interfaces\MiddlewareInterface;

/**
 * 站点升级中间件
 */
class StationOpenMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, \Closure $next)
    {
        if (!sys_config('station_open', true)) {
            return app('json')->make('410010', '站点升级中,请稍候访问');
        }
        return $next($request);
    }
}

功能说明:

  • 检查站点是否开放
  • 支持站点维护模式

3.4 安装检测中间件 (InstallMiddleware)

<?php
namespace app\http\middleware;

use app\Request;
use crmeb\interfaces\MiddlewareInterface;

class InstallMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, \Closure $next)
    {
        // 检测是否已安装CRMEB系统
        if (file_exists(root_path() . "public/install/") && !file_exists(root_path() . "public/install/install.lock")) {
            return redirect('/install/index');
        }

        return $next($request);
    }
}

功能说明:

  • 检测系统是否已安装
  • 未安装时重定向到安装页面

中间件使用方式

1. 在路由中使用

// 单个中间件
Route::get('user/info', 'UserController/info')->middleware(AuthTokenMiddleware::class);

// 带参数的中间件
Route::post('order/create', 'OrderController/create')
    ->middleware(RateLimiterMiddleware::class)
    ->option(['rateLimiter' => true, 'limitNum' => 10, 'expire' => 60]);

// 多个中间件
Route::group(function() {
    Route::get('admin/dashboard', 'AdminController/dashboard');
})->middleware([
    AdminAuthTokenMiddleware::class,
    AdminCkeckRoleMiddleware::class
]);

// 可选参数中间件
Route::get('user/agreement', 'UserController/agreement')
    ->middleware(AuthTokenMiddleware::class, false); // false表示非强制验证

2. 在控制器中使用

<?php
namespace app\controller\admin;

class UserController
{
    protected $middleware = [
        AdminAuthTokenMiddleware::class => ['except' => ['login']],
        AdminCkeckRoleMiddleware::class => ['only' => ['dashboard', 'profile']]
    ];

    public function login()
    {
        // 不需要认证中间件
    }

    public function dashboard()
    {
        // 需要认证和权限验证中间件
    }
}

3. 在应用级别使用

全局中间件在 config/middleware.php 中配置:

return [
    // 全局中间件
    \think\middleware\SessionInit::class,
    \think\middleware\LoadLangPack::class,
];

自定义中间件开发

1. 创建中间件

<?php
namespace app\http\middleware;

use app\Request;
use crmeb\interfaces\MiddlewareInterface;

class CustomMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, \Closure $next)
    {
        // 请求处理前的逻辑

        $response = $next($request);

        // 响应处理后的逻辑

        return $response;
    }
}

2. 支持参数的中间件

<?php
namespace app\http\middleware;

use app\Request;
use crmeb\interfaces\MiddlewareInterface;

class ParamMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, \Closure $next, $param1 = null, $param2 = null)
    {
        // 使用传递的参数进行处理
        if ($param1 === 'some_value') {
            // 执行特定逻辑
        }

        return $next($request);
    }
}

3. 后置中间件

<?php
namespace app\http\middleware;

use app\Request;
use crmeb\interfaces\MiddlewareInterface;

class PostMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, \Closure $next)
    {
        $response = $next($request);

        // 在响应返回前进行处理
        $response->header('X-Custom-Header', 'Custom Value');

        return $response;
    }
}

中间件执行顺序

中间件按照以下顺序执行:

  1. 全局中间件(config/middleware.php)
  2. 路由中间件(Route::middleware())
  3. 控制器中间件($middleware属性)

在同一层级中,中间件按照定义顺序执行。

最佳实践

1. 性能考虑

  • 尽量减少中间件的数量
  • 避免在中间件中执行耗时操作
  • 使用缓存减少重复计算

2. 安全性

  • 在认证中间件中严格验证token
  • 使用HTTPS传输敏感信息
  • 防止中间件绕过攻击

3. 错误处理

  • 合理处理异常情况
  • 提供有意义的错误信息
  • 记录必要的日志信息

4. 可维护性

  • 每个中间件职责单一
  • 提供清晰的文档说明
  • 使用类型提示增强代码可读性

常见问题

Q1: 中间件不执行怎么办?

检查:

  1. 中间件是否正确注册到路由或控制器
  2. 中间件类是否实现了正确的接口
  3. 路由匹配是否正确

Q2: 如何调试中间件?

可以在中间件中添加日志记录:

use think\facade\Log;

public function handle(Request $request, \Closure $next)
{
    Log::info('中间件执行', ['url' => $request->url()]);
    return $next($request);
}

Q3: 中间件如何共享数据?

通过请求对象传递数据:

public function handle(Request $request, \Closure $next)
{
    $request->customData = 'some value';
    return $next($request);
}

// 在控制器中获取
public function index(Request $request)
{
    $data = $request->customData; // 'some value'
}

Q4: 如何跳过某个中间件?

可以使用路由分组和条件判断:

Route::group(function() {
    Route::get('public/api', 'PublicController/api');
})->middleware([AllowOriginMiddleware::class]); // 只对特定路由应用

// 或在中间件内部判断
public function handle(Request $request, \Closure $next)
{
    if ($request->path() === 'public/endpoint') {
        return $next($request); // 跳过处理
    }

    // 正常处理逻辑
    return $next($request);
}
{{cateWiki.like_num}}人点赞
0人点赞
评论({{cateWiki.comment_num}}) {{commentWhere.order ? '评论从旧到新':'评论从新到旧'}} {{cateWiki.page_view_num}}人看过该文档
评论(0) {{commentWhere.order ? '评论从旧到新':'评论从新到旧'}} 9人看过该文档
评论
{{item.user ? item.user.nickname : ''}} (自评)
{{item.content}}
{{item.create_time}} 删除
{{item.like ? item.like.like_num : 0}} {{replyIndex == index ? '取消回复' : '回复'}}
评论
{{items.user ? items.user.nickname : '暂无昵称'}} (自评)
{{items.content}}
{{items.create_time}} 删除
{{items.like ? items.like.like_num : 0}} {{replyIndexJ == (index+'|'+indexJ) ? '取消回复' : '回复'}}
评论
目录
  • {{item}}