{{wikiTitle}}
中间件使用说明
CRMEB PRO 中间件使用说明
一、中间件概述
中间件是ThinkPHP的核心功能之一,用于在请求处理过程中进行预处理和后处理。CRMEB PRO通过中间件实现了认证、授权、跨域、日志等功能。
1.1 中间件目录结构
app/http/middleware/
├── AllowOriginMiddleware.php # 跨域处理中间件
├── InstallMiddleware.php # 安装检测中间件
├── StationOpenMiddleware.php # 站点开放检测中间件
├── admin/ # 后台管理中间件
│ ├── AdminAuthTokenMiddleware.php # 后台Token认证
│ ├── AdminCkeckRoleMiddleware.php # 后台权限验证
│ └── AdminLogMiddleware.php # 后台操作日志
├── api/ # 前端API中间件
│ ├── AuthTokenMiddleware.php # 用户Token认证
│ ├── BlockerMiddleware.php # 黑名单拦截
│ ├── CustomerMiddleware.php # 客服中间件
│ ├── StoreTokenMiddleware.php # 门店Token认证
│ ├── SupplierTokenMiddleware.php # 供应商Token认证
│ └── UserVisitMiddleware.php # 用户访问记录
├── kefu/ # 客服中间件
│ └── KefuAuthTokenMiddleware.php # 客服Token认证
├── out/ # 开放API中间件
│ └── OutAuthMiddleware.php # 开放接口认证
└── supplier/ # 供应商中间件
├── SupplierAuthTokenMiddleware.php # 供应商Token认证
├── SupplierCheckRoleMiddleware.php # 供应商权限验证
└── SupplierLogMiddleware.php # 供应商操作日志
1.2 中间件接口
所有中间件都需要实现 MiddlewareInterface 接口:
namespace crmeb\interfaces;
use app\Request;
interface MiddlewareInterface
{
/**
* 中间件处理方法
* @param Request $request 请求对象
* @param \Closure $next 下一个中间件
* @return mixed
*/
public function handle(Request $request, \Closure $next);
}
二、全局中间件
2.1 配置文件
全局中间件在 app/middleware.php 中配置:
return [
// Session初始化
\think\middleware\SessionInit::class,
// 多语言初始化
\think\middleware\LoadLangPack::class,
// 全局请求缓存(按需开启)
// \think\middleware\CheckRequestCache::class,
// 页面Trace调试(开发环境)
// \think\middleware\TraceDebug::class,
];
2.2 AllowOriginMiddleware - 跨域中间件
处理跨域请求,设置CORS响应头。
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;
}
// OPTIONS预检请求直接返回
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;
}
}
配置跨域响应头:
// config/cookie.php
return [
'header' => [
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Headers' => 'Authori-zation, Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With, Form-type',
'Access-Control-Allow-Methods' => 'GET, POST, PATCH, PUT, DELETE, OPTIONS',
'Access-Control-Max-Age' => '1728000',
],
];
2.3 InstallMiddleware - 安装检测中间件
检测系统是否已安装。
namespace app\http\middleware;
use app\Request;
use crmeb\interfaces\MiddlewareInterface;
class InstallMiddleware implements MiddlewareInterface
{
public function handle(Request $request, \Closure $next)
{
// 检查安装锁文件
$installLock = root_path() . 'public/install/install.lock';
if (!file_exists($installLock)) {
// 未安装,跳转到安装页面
return redirect('/install');
}
return $next($request);
}
}
三、后台管理中间件
3.1 AdminAuthTokenMiddleware - Token认证
验证后台管理员的登录状态。
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)
{
// 获取Token
$token = trim(ltrim($request->header(Config::get('cookie.token_name', 'Authori-zation')), 'Bearer'));
/** @var AdminAuthServices $service */
$service = app()->make(AdminAuthServices::class);
// 解析Token获取管理员信息
$adminInfo = $service->parseToken($token);
// 将管理员信息注入到请求对象
$request->isAdminLogin = !is_null($adminInfo);
$request->adminId = $adminInfo['id'];
$request->adminInfo = $adminInfo;
$request->adminType = $adminInfo['admin_type'];
return $next($request);
}
}
3.2 AdminCkeckRoleMiddleware - 权限验证
验证管理员的操作权限。
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']) {
/** @var SystemRoleServices $systemRoleService */
$systemRoleService = app()->make(SystemRoleServices::class);
$systemRoleService->verifiAuth($request);
}
return $next($request);
}
}
3.3 AdminLogMiddleware - 操作日志
记录后台管理员的操作日志。
namespace app\http\middleware\admin;
use app\Request;
use app\services\system\log\SystemLogServices;
use crmeb\interfaces\MiddlewareInterface;
/**
* 后台操作日志中间件
*/
class AdminLogMiddleware implements MiddlewareInterface
{
public function handle(Request $request, \Closure $next)
{
// 先执行请求
$response = $next($request);
// 记录操作日志(POST、PUT、DELETE请求)
if (in_array($request->method(), ['POST', 'PUT', 'DELETE'])) {
$this->recordLog($request);
}
return $response;
}
/**
* 记录日志
*/
protected function recordLog(Request $request)
{
try {
/** @var SystemLogServices $logServices */
$logServices = app()->make(SystemLogServices::class);
$logServices->create([
'admin_id' => $request->adminId(),
'admin_name' => $request->adminInfo()['account'] ?? '',
'path' => $request->pathinfo(),
'method' => $request->method(),
'ip' => $request->ip(),
'user_agent' => $request->header('user-agent'),
'add_time' => time(),
]);
} catch (\Throwable $e) {
// 日志记录失败不影响请求
}
}
}
四、前端API中间件
4.1 AuthTokenMiddleware - 用户Token认证
验证前端用户的登录状态。
namespace app\http\middleware\api;
use app\Request;
use app\services\user\UserAuthServices;
use crmeb\exceptions\AuthException;
use crmeb\interfaces\MiddlewareInterface;
/**
* 前端用户Token认证中间件
*/
class AuthTokenMiddleware implements MiddlewareInterface
{
/**
* @param Request $request
* @param \Closure $next
* @param bool $force 是否强制验证(true-未登录抛异常,false-允许未登录)
*/
public function handle(Request $request, \Closure $next, bool $force = true)
{
$authInfo = null;
// 获取Token(兼容两种header名称)
$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'];
}
$request->isLogin = !is_null($authInfo);
$request->uid = is_null($authInfo) ? 0 : (int)$authInfo['user']->uid;
return $next($request);
}
}
4.2 BlockerMiddleware - 黑名单拦截
拦截黑名单用户的请求。
namespace app\http\middleware\api;
use app\Request;
use app\services\user\UserServices;
use crmeb\interfaces\MiddlewareInterface;
/**
* 黑名单拦截中间件
*/
class BlockerMiddleware implements MiddlewareInterface
{
public function handle(Request $request, \Closure $next)
{
$uid = $request->uid();
if ($uid) {
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$userInfo = $userServices->get($uid);
// 检查用户状态
if ($userInfo && $userInfo['status'] == 0) {
return app('json')->fail('您的账号已被禁用');
}
// 检查是否在黑名单
if ($userInfo && $userInfo['is_black'] == 1) {
return app('json')->fail('您已被加入黑名单');
}
}
return $next($request);
}
}
4.3 UserVisitMiddleware - 用户访问记录
记录用户的访问行为。
namespace app\http\middleware\api;
use app\Request;
use app\services\user\UserVisitServices;
use crmeb\interfaces\MiddlewareInterface;
/**
* 用户访问记录中间件
*/
class UserVisitMiddleware implements MiddlewareInterface
{
public function handle(Request $request, \Closure $next)
{
$response = $next($request);
// 记录用户访问
if ($request->uid()) {
try {
/** @var UserVisitServices $visitServices */
$visitServices = app()->make(UserVisitServices::class);
$visitServices->create([
'uid' => $request->uid(),
'url' => $request->pathinfo(),
'ip' => $request->ip(),
'channel_type' => $this->getChannelType($request),
'add_time' => time(),
]);
} catch (\Throwable $e) {
// 记录失败不影响请求
}
}
return $response;
}
/**
* 获取访问渠道
*/
protected function getChannelType(Request $request): string
{
$userAgent = strtolower($request->header('user-agent', ''));
if (strpos($userAgent, 'miniprogram') !== false) {
return 'routine'; // 小程序
} elseif (strpos($userAgent, 'micromessenger') !== false) {
return 'wechat'; // 微信
} elseif ($request->isApp()) {
return 'app'; // APP
} else {
return 'h5'; // H5
}
}
}
五、路由中间件配置
5.1 在路由中使用中间件
// route/admin.php
use think\facade\Route;
use app\http\middleware\AllowOriginMiddleware;
use app\http\middleware\admin\AdminAuthTokenMiddleware;
use app\http\middleware\admin\AdminCkeckRoleMiddleware;
use app\http\middleware\admin\AdminLogMiddleware;
// 后台路由组
Route::group('adminapi', function() {
// 不需要登录的路由
Route::group('login', function() {
Route::post('login', 'Login/login');
Route::get('captcha', 'Login/captcha');
});
// 需要登录的路由
Route::group(function() {
Route::get('user/list', 'User/list');
Route::post('user/add', 'User/add');
// ... 其他路由
})->middleware([
AdminAuthTokenMiddleware::class,
AdminCkeckRoleMiddleware::class,
AdminLogMiddleware::class,
]);
})->middleware(AllowOriginMiddleware::class)
->prefix('admin.');
5.2 前端API路由中间件
// route/api.php
use think\facade\Route;
use app\http\middleware\AllowOriginMiddleware;
use app\http\middleware\api\AuthTokenMiddleware;
use app\http\middleware\api\BlockerMiddleware;
Route::group('api', function() {
// 公开接口(不需要登录)
Route::group(function() {
Route::get('index', 'Index/index');
Route::get('product/list', 'Product/list');
})->middleware(AuthTokenMiddleware::class, false); // false表示不强制登录
// 需要登录的接口
Route::group(function() {
Route::get('user/info', 'User/info');
Route::post('order/create', 'Order/create');
})->middleware([
[AuthTokenMiddleware::class, true], // true表示强制登录
BlockerMiddleware::class,
]);
})->middleware(AllowOriginMiddleware::class)
->prefix('api.');
5.3 带参数的中间件
// 中间件定义
class AuthTokenMiddleware implements MiddlewareInterface
{
public function handle(Request $request, \Closure $next, bool $force = true)
{
// $force 参数控制是否强制验证
}
}
// 路由中使用
Route::get('user/info', 'User/info')
->middleware(AuthTokenMiddleware::class, true); // 传递参数true
Route::get('product/list', 'Product/list')
->middleware(AuthTokenMiddleware::class, false); // 传递参数false
// 数组方式传参
Route::group(function() {
// ...
})->middleware([
[AuthTokenMiddleware::class, true],
[RateLimitMiddleware::class, 100, 60], // 多个参数
]);
六、自定义中间件开发
6.1 创建中间件
namespace app\http\middleware;
use app\Request;
use crmeb\interfaces\MiddlewareInterface;
/**
* 请求频率限制中间件
*/
class RateLimitMiddleware implements MiddlewareInterface
{
/**
* @param Request $request
* @param \Closure $next
* @param int $maxRequests 最大请求次数
* @param int $decaySeconds 时间窗口(秒)
*/
public function handle(Request $request, \Closure $next, int $maxRequests = 60, int $decaySeconds = 60)
{
// 生成限流键
$key = $this->getRateLimitKey($request);
// 检查是否超过限制
if ($this->tooManyAttempts($key, $maxRequests)) {
return app('json')->fail('请求过于频繁,请稍后再试', [], 429);
}
// 记录请求
$this->hit($key, $decaySeconds);
// 执行请求
$response = $next($request);
// 添加响应头
return $response->header([
'X-RateLimit-Limit' => $maxRequests,
'X-RateLimit-Remaining' => $this->remaining($key, $maxRequests),
]);
}
/**
* 生成限流键
*/
protected function getRateLimitKey(Request $request): string
{
$uid = $request->uid() ?: 0;
$ip = $request->ip();
$path = $request->pathinfo();
return 'rate_limit:' . md5($uid . $ip . $path);
}
/**
* 检查是否超过限制
*/
protected function tooManyAttempts(string $key, int $maxRequests): bool
{
$attempts = cache($key) ?: 0;
return $attempts >= $maxRequests;
}
/**
* 记录请求
*/
protected function hit(string $key, int $decaySeconds): void
{
$attempts = cache($key) ?: 0;
cache($key, $attempts + 1, $decaySeconds);
}
/**
* 获取剩余次数
*/
protected function remaining(string $key, int $maxRequests): int
{
$attempts = cache($key) ?: 0;
return max(0, $maxRequests - $attempts);
}
}
6.2 日志中间件
namespace app\http\middleware;
use app\Request;
use crmeb\interfaces\MiddlewareInterface;
use think\facade\Log;
/**
* 请求日志中间件
*/
class RequestLogMiddleware implements MiddlewareInterface
{
public function handle(Request $request, \Closure $next)
{
// 记录请求开始时间
$startTime = microtime(true);
// 执行请求
$response = $next($request);
// 计算执行时间
$duration = round((microtime(true) - $startTime) * 1000, 2);
// 记录日志
Log::channel('request')->info('API Request', [
'method' => $request->method(),
'path' => $request->pathinfo(),
'params' => $request->param(),
'ip' => $request->ip(),
'uid' => $request->uid() ?? 0,
'duration' => $duration . 'ms',
'status' => $response->getCode(),
]);
return $response;
}
}
6.3 数据验证中间件
namespace app\http\middleware;
use app\Request;
use crmeb\interfaces\MiddlewareInterface;
use think\exception\ValidateException;
/**
* 请求数据验证中间件
*/
class ValidateMiddleware implements MiddlewareInterface
{
/**
* @param Request $request
* @param \Closure $next
* @param string $validate 验证器类名
* @param string $scene 验证场景
*/
public function handle(Request $request, \Closure $next, string $validate, string $scene = '')
{
$data = $request->param();
// 实例化验证器
$v = app()->make($validate);
// 设置场景
if ($scene) {
$v->scene($scene);
}
// 执行验证
if (!$v->check($data)) {
throw new ValidateException($v->getError());
}
return $next($request);
}
}
// 使用示例
Route::post('user/register', 'User/register')
->middleware(ValidateMiddleware::class, 'app\\validate\\UserValidate', 'register');
七、中间件执行顺序
7.1 执行顺序说明
请求进入
│
▼
全局中间件(app/middleware.php)
│
▼
路由中间件(按定义顺序)
│
▼
控制器中间件(如有)
│
▼
控制器方法
│
▼
控制器中间件(后置)
│
▼
路由中间件(后置)
│
▼
全局中间件(后置)
│
▼
响应返回
7.2 前置和后置处理
class ExampleMiddleware implements MiddlewareInterface
{
public function handle(Request $request, \Closure $next)
{
// ========== 前置处理 ==========
// 在请求处理之前执行
$this->beforeHandle($request);
// ========== 执行请求 ==========
$response = $next($request);
// ========== 后置处理 ==========
// 在请求处理之后执行
$this->afterHandle($request, $response);
return $response;
}
protected function beforeHandle(Request $request)
{
// 前置逻辑
}
protected function afterHandle(Request $request, $response)
{
// 后置逻辑
}
}
八、中间件最佳实践
8.1 命名规范
// 中间件命名规范:功能 + Middleware
AdminAuthTokenMiddleware // 后台Token认证
ApiRateLimitMiddleware // API频率限制
RequestLogMiddleware // 请求日志
DataValidateMiddleware // 数据验证
8.2 异常处理
class SafeMiddleware implements MiddlewareInterface
{
public function handle(Request $request, \Closure $next)
{
try {
return $next($request);
} catch (AuthException $e) {
// 认证异常
return app('json')->fail($e->getMessage(), [], $e->getCode());
} catch (ValidateException $e) {
// 验证异常
return app('json')->fail($e->getMessage(), [], 422);
} catch (\Throwable $e) {
// 其他异常
Log::error('中间件异常: ' . $e->getMessage());
return app('json')->fail('系统错误', [], 500);
}
}
}
8.3 中间件复用
// 定义通用的认证特性
trait AuthTrait
{
protected function parseToken(Request $request, string $tokenName): ?array
{
$token = trim(ltrim($request->header($tokenName), 'Bearer'));
if (!$token) {
return null;
}
// Token解析逻辑...
}
}
// 在不同中间件中复用
class AdminAuthTokenMiddleware implements MiddlewareInterface
{
use AuthTrait;
public function handle(Request $request, \Closure $next)
{
$authInfo = $this->parseToken($request, 'Authori-zation');
// ...
}
}
class ApiAuthTokenMiddleware implements MiddlewareInterface
{
use AuthTrait;
public function handle(Request $request, \Closure $next)
{
$authInfo = $this->parseToken($request, 'Authorization');
// ...
}
}
九、常用中间件配置参考
| 中间件 | 位置 | 功能 | 参数 |
|---|---|---|---|
| AllowOriginMiddleware | 全局 | 跨域处理 | 无 |
| InstallMiddleware | 全局 | 安装检测 | 无 |
| AdminAuthTokenMiddleware | admin路由 | 后台认证 | 无 |
| AdminCkeckRoleMiddleware | admin路由 | 权限验证 | 无 |
| AdminLogMiddleware | admin路由 | 操作日志 | 无 |
| AuthTokenMiddleware | api路由 | 用户认证 | bool $force |
| BlockerMiddleware | api路由 | 黑名单拦截 | 无 |
| UserVisitMiddleware | api路由 | 访问记录 | 无 |
注意事项
- 中间件执行顺序:全局中间件优先于路由中间件执行,中间件的注册顺序决定了执行顺序,请合理安排中间件的配置位置
- Token 命名差异:后台管理使用
Authori-zation(带连字符),前端API使用Authorization(标准写法),开发时注意区分 - 强制认证参数:
AuthTokenMiddleware的$force参数设为true时,未登录用户将被拦截;设为false时允许游客访问但不注入用户信息 - 跨域配置:
AllowOriginMiddleware默认允许所有来源,生产环境建议配置具体的允许域名列表 - 日志记录:
AdminLogMiddleware会记录所有后台操作,敏感接口可能需要额外的日志脱敏处理 - 中间件异常处理:中间件中抛出的异常会被
ExceptionHandle统一捕获,确保返回格式一致 - 性能考虑:避免在中间件中执行耗时操作,如复杂的数据库查询或外部API调用
- Swoole 环境:在 Swoole 协程环境下,中间件中使用的静态变量和单例需要注意数据隔离问题
常见问题
Q: 为什么前端请求总是返回”请登录”错误?
A: 检查以下几点:1) 确认请求头中携带了正确的 Token 字段名(后台用Authori-zation,前端用Authorization);2) 确认 Token 未过期;3) 确认 Token 格式正确,需要带Bearer前缀Q: 如何跳过某个接口的认证中间件?
A: 在AuthTokenMiddleware中维护一个白名单数组,将不需要认证的路由添加到白名单中;或者在路由定义时不添加认证中间件Q: 自定义中间件如何获取当前登录用户信息?
A: 在认证中间件执行后,可以通过$request->uid()获取用户ID,通过$request->user()获取用户完整信息Q: 跨域请求 OPTIONS 预检失败怎么办?
A: 确保AllowOriginMiddleware在中间件链的最前面执行,并且正确处理了 OPTIONS 请求方法,返回正确的 CORS 头信息Q: 中间件中如何向控制器传递数据?
A: 使用$request->withAttribute('key', $value)设置数据,在控制器中通过$request->getAttribute('key')获取Q: 如何记录中间件的执行时间用于性能分析?
A: 在中间件的handle方法开头记录开始时间,在$next($request)执行后计算耗时并记录到日志中
