ThinkPHP8中间件实战应用:登录检测、跨域处理与多功能实现
Orion K Lv6

在Web开发中,中间件作为处理HTTP请求和响应的关键组件,扮演着至关重要的角色。ThinkPHP8提供了强大的中间件支持,使得开发者能够轻松实现各种功能,如登录检测、多语言切换和跨域请求处理等 2。本文将通过真实案例,详细讲解如何在ThinkPHP8中间件中实现这些功能。

中间件基础概念

中间件的作用

中间件主要用于拦截或过滤应用的HTTP请求,并进行必要的业务处理 1。我们可以在不修改应用程序逻辑的情况下添加额外的功能,如:

  • 登录验证:检查用户是否已登录
  • 权限控制:验证用户访问权限
  • 请求过滤:过滤恶意请求
  • 环境判断:判断当前浏览器环境是在微信或支付宝
  • 系统日志:记录访问日志和统计信息
  • 跨域处理:处理CORS跨域请求

中间件类型

ThinkPHP8的中间件分为4种类型 1

  1. 全局中间件:对所有请求生效
  2. 应用中间件:对特定应用生效
  3. 路由中间件:对特定路由生效
  4. 控制器中间件:对特定控制器生效

创建中间件

使用命令行创建

1
2
3
4
5
6
7
8
9
10
11
# 单应用创建中间件
php think make:middleware Check

# 多应用创建中间件
php think make:middleware Index@Check

# 创建登录检测中间件
php think make:middleware Auth

# 创建跨域处理中间件
php think make:middleware Cors

中间件基本结构

中间件的入口执行方法必须是handle方法,第一个参数是Request对象,第二个参数是一个闭包 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
declare(strict_types=1);

namespace app\middleware;

use think\Request;
use think\Response;
use Closure;

/**
* 基础中间件类
* 提供中间件的基本结构和方法
*/
class BaseMiddleware
{
/**
* 处理请求
* @param Request $request 请求对象
* @param Closure $next 下一个中间件闭包
* @return Response 响应对象
*/
public function handle(Request $request, Closure $next): Response
{
// 前置处理逻辑

// 执行下一个中间件或控制器
$response = $next($request);

// 后置处理逻辑

return $response;
}
}

登录检测中间件实现

创建用户认证中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
<?php
declare(strict_types=1);

namespace app\middleware;

use think\Request;
use think\Response;
use think\facade\Session;
use think\facade\Cookie;
use think\exception\HttpResponseException;
use Closure;

/**
* 用户认证中间件
* 检查用户登录状态和权限
*/
class AuthMiddleware
{
/**
* 处理用户认证
* @param Request $request 请求对象
* @param Closure $next 下一个中间件闭包
* @param string $guard 认证守卫
* @return Response 响应对象
* @throws HttpResponseException 认证失败异常
*/
public function handle(Request $request, Closure $next, string $guard = 'user'): Response
{
// 检查Session中的用户信息
$userId = Session::get($guard . '_id');
$userInfo = Session::get($guard . '_info');

// 如果Session中没有用户信息,检查Cookie中的记住我token
if (!$userId && $this->hasRememberToken($request)) {
$userId = $this->validateRememberToken($request, $guard);
}

// 用户未登录
if (!$userId) {
return $this->handleUnauthenticated($request);
}

// 验证用户状态
if (!$this->validateUserStatus($userId)) {
$this->clearUserSession($guard);
return $this->handleUnauthenticated($request, '账户已被禁用');
}

// 更新最后活动时间
$this->updateLastActivity($userId);

// 将用户信息注入到请求中
$request->withMiddleware([
'user_id' => $userId,
'user_info' => $userInfo,
'guard' => $guard
]);

return $next($request);
}

/**
* 检查是否有记住我token
* @param Request $request 请求对象
* @return bool 是否有token
*/
private function hasRememberToken(Request $request): bool
{
return !empty(Cookie::get('remember_token'));
}

/**
* 验证记住我token
* @param Request $request 请求对象
* @param string $guard 认证守卫
* @return int|null 用户ID
*/
private function validateRememberToken(Request $request, string $guard): ?int
{
$token = Cookie::get('remember_token');

// 这里应该从数据库验证token的有效性
// 简化示例,实际项目中需要完善token验证逻辑
$userId = $this->getUserIdByToken($token);

if ($userId) {
// 重新设置Session
Session::set($guard . '_id', $userId);
Session::set($guard . '_info', $this->getUserInfo($userId));
}

return $userId;
}

/**
* 验证用户状态
* @param int $userId 用户ID
* @return bool 用户状态是否正常
*/
private function validateUserStatus(int $userId): bool
{
// 从数据库检查用户状态
// 这里简化处理,实际项目中需要查询数据库
return true;
}

/**
* 更新用户最后活动时间
* @param int $userId 用户ID
* @return void
*/
private function updateLastActivity(int $userId): void
{
// 更新用户最后活动时间到数据库
// 可以使用队列异步处理以提高性能
}

/**
* 清除用户Session
* @param string $guard 认证守卫
* @return void
*/
private function clearUserSession(string $guard): void
{
Session::delete($guard . '_id');
Session::delete($guard . '_info');
Cookie::delete('remember_token');
}

/**
* 处理未认证的请求
* @param Request $request 请求对象
* @param string $message 错误消息
* @return Response 响应对象
* @throws HttpResponseException 认证失败异常
*/
private function handleUnauthenticated(Request $request, string $message = '请先登录'): Response
{
// AJAX请求返回JSON
if ($request->isAjax()) {
$response = json([
'code' => 401,
'msg' => $message,
'data' => null
]);
throw new HttpResponseException($response);
}

// 普通请求重定向到登录页
$loginUrl = url('login', ['redirect' => $request->url()]);
$response = redirect($loginUrl);
throw new HttpResponseException($response);
}

/**
* 根据token获取用户ID
* @param string $token 记住我token
* @return int|null 用户ID
*/
private function getUserIdByToken(string $token): ?int
{
// 实际项目中需要从数据库查询
return null;
}

/**
* 获取用户信息
* @param int $userId 用户ID
* @return array 用户信息
*/
private function getUserInfo(int $userId): array
{
// 实际项目中需要从数据库查询
return [];
}
}

Session初始化配置

在ThinkPHP8中,Session默认是未开启的,需要在中间件配置中注册 1

1
2
3
4
5
6
7
8
<?php
// app/middleware.php
return [
// Session初始化
\think\middleware\SessionInit::class,

// 其他中间件...
];

跨域处理中间件

CORS跨域中间件实现

ThinkPHP提供了多种解决跨域的方式 4,推荐使用中间件统一处理跨域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
<?php
declare(strict_types=1);

namespace app\middleware;

use think\Request;
use think\Response;
use Closure;

/**
* CORS跨域处理中间件
* 统一处理跨域请求和预检请求
*/
class CorsMiddleware
{
/**
* 允许的域名配置
* @var array
*/
private array $allowedOrigins = [
'http://localhost:3000',
'http://localhost:8080',
'https://yourdomain.com',
// 开发环境可以使用 '*' 允许所有域名
];

/**
* 允许的请求方法
* @var array
*/
private array $allowedMethods = [
'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'
];

/**
* 允许的请求头
* @var array
*/
private array $allowedHeaders = [
'Content-Type',
'Authorization',
'X-Requested-With',
'Accept',
'Origin',
'Access-Control-Request-Method',
'Access-Control-Request-Headers',
'token',
'x-token'
];

/**
* 处理跨域请求
* @param Request $request 请求对象
* @param Closure $next 下一个中间件闭包
* @return Response 响应对象
*/
public function handle(Request $request, Closure $next): Response
{
// 获取请求来源
$origin = $request->header('Origin', '');

// 处理预检请求 (OPTIONS)
if ($request->method() === 'OPTIONS') {
return $this->handlePreflightRequest($origin);
}

// 处理实际请求
$response = $next($request);

// 添加CORS响应头
return $this->addCorsHeaders($response, $origin);
}

/**
* 处理预检请求
* @param string $origin 请求来源
* @return Response 响应对象
*/
private function handlePreflightRequest(string $origin): Response
{
$response = response('', 200);

// 设置预检请求的响应头
$headers = [
'Access-Control-Allow-Origin' => $this->getAllowedOrigin($origin),
'Access-Control-Allow-Methods' => implode(', ', $this->allowedMethods),
'Access-Control-Allow-Headers' => implode(', ', $this->allowedHeaders),
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Max-Age' => '86400', // 预检请求缓存时间
];

foreach ($headers as $key => $value) {
$response->header($key, $value);
}

return $response;
}

/**
* 添加CORS响应头
* @param Response $response 响应对象
* @param string $origin 请求来源
* @return Response 响应对象
*/
private function addCorsHeaders(Response $response, string $origin): Response
{
$headers = [
'Access-Control-Allow-Origin' => $this->getAllowedOrigin($origin),
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Expose-Headers' => 'Content-Length, Content-Type',
];

foreach ($headers as $key => $value) {
$response->header($key, $value);
}

return $response;
}

/**
* 获取允许的来源域名
* @param string $origin 请求来源
* @return string 允许的来源
*/
private function getAllowedOrigin(string $origin): string
{
// 如果配置了通配符,直接返回
if (in_array('*', $this->allowedOrigins)) {
return '*';
}

// 检查来源是否在允许列表中
if (in_array($origin, $this->allowedOrigins)) {
return $origin;
}

// 支持子域名匹配
foreach ($this->allowedOrigins as $allowedOrigin) {
if ($this->matchesOrigin($origin, $allowedOrigin)) {
return $origin;
}
}

// 默认返回第一个允许的域名
return $this->allowedOrigins[0] ?? '';
}

/**
* 匹配来源域名
* @param string $origin 请求来源
* @param string $allowedOrigin 允许的来源模式
* @return bool 是否匹配
*/
private function matchesOrigin(string $origin, string $allowedOrigin): bool
{
// 支持通配符子域名匹配,如 *.example.com
if (strpos($allowedOrigin, '*.') === 0) {
$pattern = str_replace('*.', '', $allowedOrigin);
return str_ends_with($origin, $pattern);
}

return $origin === $allowedOrigin;
}
}

权限控制中间件

RBAC权限检查中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
<?php
declare(strict_types=1);

namespace app\middleware;

use think\Request;
use think\Response;
use think\facade\Session;
use think\exception\HttpResponseException;
use Closure;

/**
* RBAC权限控制中间件
* 基于角色的访问控制
*/
class RbacMiddleware
{
/**
* 处理权限检查
* @param Request $request 请求对象
* @param Closure $next 下一个中间件闭包
* @param string $permission 所需权限
* @return Response 响应对象
* @throws HttpResponseException 权限不足异常
*/
public function handle(Request $request, Closure $next, string $permission = ''): Response
{
$userId = Session::get('user_id');

// 检查用户是否登录
if (!$userId) {
return $this->handleUnauthorized($request, '请先登录');
}

// 获取当前路由权限标识
if (empty($permission)) {
$permission = $this->getRoutePermission($request);
}

// 检查用户权限
if (!$this->checkUserPermission($userId, $permission)) {
return $this->handleUnauthorized($request, '权限不足');
}

// 记录权限访问日志
$this->logPermissionAccess($userId, $permission, $request);

return $next($request);
}

/**
* 获取路由权限标识
* @param Request $request 请求对象
* @return string 权限标识
*/
private function getRoutePermission(Request $request): string
{
$controller = $request->controller();
$action = $request->action();

// 生成权限标识,如:user.create, article.edit
return strtolower($controller) . '.' . strtolower($action);
}

/**
* 检查用户权限
* @param int $userId 用户ID
* @param string $permission 权限标识
* @return bool 是否有权限
*/
private function checkUserPermission(int $userId, string $permission): bool
{
// 超级管理员拥有所有权限
if ($this->isSuperAdmin($userId)) {
return true;
}

// 从缓存或数据库获取用户权限列表
$userPermissions = $this->getUserPermissions($userId);

// 检查是否有对应权限
return in_array($permission, $userPermissions);
}

/**
* 检查是否为超级管理员
* @param int $userId 用户ID
* @return bool 是否为超级管理员
*/
private function isSuperAdmin(int $userId): bool
{
// 实际项目中从数据库查询
$superAdminIds = [1]; // 超级管理员ID列表
return in_array($userId, $superAdminIds);
}

/**
* 获取用户权限列表
* @param int $userId 用户ID
* @return array 权限列表
*/
private function getUserPermissions(int $userId): array
{
// 实际项目中应该从缓存或数据库获取
// 这里返回示例权限
return [
'user.list',
'user.create',
'article.list',
'article.edit'
];
}

/**
* 记录权限访问日志
* @param int $userId 用户ID
* @param string $permission 权限标识
* @param Request $request 请求对象
* @return void
*/
private function logPermissionAccess(int $userId, string $permission, Request $request): void
{
// 记录访问日志到数据库或日志文件
// 可以使用队列异步处理
$logData = [
'user_id' => $userId,
'permission' => $permission,
'url' => $request->url(),
'method' => $request->method(),
'ip' => $request->ip(),
'user_agent' => $request->header('User-Agent'),
'created_at' => date('Y-m-d H:i:s')
];

// 异步记录日志
// Queue::push('LogPermissionAccess', $logData);
}

/**
* 处理权限不足的请求
* @param Request $request 请求对象
* @param string $message 错误消息
* @return Response 响应对象
* @throws HttpResponseException 权限不足异常
*/
private function handleUnauthorized(Request $request, string $message): Response
{
if ($request->isAjax()) {
$response = json([
'code' => 403,
'msg' => $message,
'data' => null
]);
} else {
$response = view('error/403', ['message' => $message]);
}

throw new HttpResponseException($response);
}
}

中间件注册与使用

全局中间件注册

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

1
2
3
4
5
6
7
8
9
10
<?php
return [
// Session初始化(必须放在最前面)
\think\middleware\SessionInit::class,

// CORS跨域处理
\app\middleware\CorsMiddleware::class,

// 其他全局中间件...
];

路由中间件使用

在路由定义中使用中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
// route/app.php
use think\facade\Route;

// 单个路由使用中间件
Route::get('user/profile', 'User/profile')
->middleware(\app\middleware\AuthMiddleware::class);

// 路由分组使用中间件
Route::group('admin', function () {
Route::get('dashboard', 'Admin/dashboard');
Route::get('users', 'Admin/users');
Route::post('user/create', 'Admin/createUser');
})->middleware([
\app\middleware\AuthMiddleware::class,
\app\middleware\RbacMiddleware::class
]);

// 带参数的中间件
Route::post('admin/sensitive', 'Admin/sensitive')
->middleware(\app\middleware\RbacMiddleware::class, 'admin.sensitive');

// 使用allowCrossDomain方法(ThinkPHP推荐)
Route::get('api/data', 'Api/data')->allowCrossDomain();

控制器中间件使用

在控制器中定义中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?php
declare(strict_types=1);

namespace app\controller;

use think\Request;
use think\Response;
use app\middleware\AuthMiddleware;
use app\middleware\RbacMiddleware;

/**
* 用户控制器
* 演示控制器级别的中间件使用
*/
class UserController
{
/**
* 控制器中间件定义
* @var array
*/
protected array $middleware = [
// 所有方法都需要登录
AuthMiddleware::class,

// 特定方法需要权限检查
RbacMiddleware::class => ['only' => ['delete', 'edit']],

// 排除特定方法
// RbacMiddleware::class => ['except' => ['index', 'show']],
];

/**
* 用户列表
* @param Request $request 请求对象
* @return Response 响应对象
*/
public function index(Request $request): Response
{
// 获取中间件注入的用户信息
$userId = $request->middleware('user_id');
$userInfo = $request->middleware('user_info');

return json([
'code' => 200,
'msg' => 'success',
'data' => [
'current_user' => $userInfo,
'users' => []
]
]);
}

/**
* 删除用户(需要权限)
* @param Request $request 请求对象
* @return Response 响应对象
*/
public function delete(Request $request): Response
{
$id = $request->param('id');

// 删除用户逻辑

return json([
'code' => 200,
'msg' => '删除成功'
]);
}
}

中间件最佳实践

1. 性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
// 使用缓存减少数据库查询
use think\facade\Cache;

class OptimizedAuthMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$userId = Session::get('user_id');

if ($userId) {
// 使用缓存获取用户信息
$cacheKey = 'user_info_' . $userId;
$userInfo = Cache::remember($cacheKey, function() use ($userId) {
return $this->getUserFromDatabase($userId);
}, 300); // 缓存5分钟

$request->withMiddleware(['user_info' => $userInfo]);
}

return $next($request);
}
}

2. 错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class SafeMiddleware
{
public function handle(Request $request, Closure $next): Response
{
try {
// 中间件逻辑
return $next($request);
} catch (\Exception $e) {
// 记录错误日志
\think\facade\Log::error('中间件执行错误: ' . $e->getMessage());

// 返回友好的错误响应
if ($request->isAjax()) {
return json([
'code' => 500,
'msg' => '系统错误,请稍后重试'
]);
}

return view('error/500');
}
}
}

3. 中间件参数传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class ParameterMiddleware
{
public function handle(Request $request, Closure $next, ...$params): Response
{
// 处理多个参数
[$permission, $level] = $params;

// 根据参数执行不同逻辑
if ($level === 'strict') {
// 严格模式检查
}

return $next($request);
}
}

// 路由中使用
Route::get('admin/config', 'Admin/config')
->middleware(ParameterMiddleware::class, 'admin.config', 'strict');

总结

ThinkPHP8的中间件系统提供了强大而灵活的请求处理机制。通过合理使用中间件,我们可以:

  1. 统一处理跨域问题:使用CORS中间件统一处理跨域请求
  2. 实现用户认证:通过认证中间件检查用户登录状态
  3. 控制访问权限:使用RBAC中间件实现细粒度权限控制
  4. 提升代码复用性:将通用逻辑抽取到中间件中
  5. 增强系统安全性:在中间件中实现安全检查和过滤

在实际项目中,建议根据业务需求合理选择中间件类型和使用方式,注意性能优化和错误处理,确保系统的稳定性和可维护性。中间件的合理使用能够让ThinkPHP8应用更加健壮和高效。

本站由 提供部署服务