sunxbiao преди 7 месеца
родител
ревизия
29b138d888
променени са 2 файла, в които са добавени 484 реда и са изтрити 0 реда
  1. 478 0
      app/middleware/SecurityMiddleware.php
  2. 6 0
      config/middleware.php

+ 478 - 0
app/middleware/SecurityMiddleware.php

@@ -0,0 +1,478 @@
1
+<?php
2
+
3
+declare(strict_types=1);
4
+
5
+namespace app\middleware;
6
+
7
+use think\facade\Db;
8
+use think\facade\Log;
9
+use think\facade\Cache;
10
+use think\Request;
11
+use think\Response;
12
+
13
+/**
14
+ * 安全防护中间件
15
+ */
16
+class SecurityMiddleware
17
+{
18
+    // 配置参数
19
+    protected $config = [
20
+        'enable_logging' => true,        // 是否记录攻击日志
21
+        'enable_blocking' => true,       // 是否自动封禁攻击IP
22
+        'max_param_length' => 500,       // 参数最大长度(超过部分截断)
23
+        'max_logs_per_ip' => 100,        // 单个IP最大攻击记录数
24
+        'block_duration' => 86400,       // 封禁时长(秒),默认24小时
25
+        'attack_threshold' => 10,        // 攻击阈值(超过此次数自动封禁)
26
+    ];
27
+
28
+    /**
29
+     * 处理请求
30
+     */
31
+    public function handle(Request $request, \Closure $next)
32
+    {
33
+        // 检查IP是否已被封禁
34
+        if ($this->isIpBlocked($request->ip())) {
35
+            $this->logBlockedAccess($request);
36
+            return $this->blockResponse();
37
+        }
38
+
39
+        // 安全检查
40
+        $attackDetected = $this->checkSecurity($request);
41
+
42
+        if ($attackDetected) {
43
+            // 记录攻击并可能封禁IP
44
+            $this->handleAttack($request, $attackDetected);
45
+
46
+            // 返回通用错误,不泄露信息
47
+            return $this->errorResponse();
48
+        }
49
+
50
+        return $next($request);
51
+    }
52
+
53
+    /**
54
+     * 安全检查
55
+     */
56
+    protected function checkSecurity(Request $request): array
57
+    {
58
+        $attacks = [];
59
+
60
+        // 检查GET参数
61
+        foreach ($request->get() as $key => $value) {
62
+            if ($this->detectAttack($value, $key)) {
63
+                $attacks[] = [
64
+                    'type' => $this->detectAttackType($value),
65
+                    'param' => $key,
66
+                    'value' => $value
67
+                ];
68
+            }
69
+        }
70
+
71
+        // 检查POST参数
72
+        foreach ($request->post() as $key => $value) {
73
+            if ($this->detectAttack($value, $key)) {
74
+                $attacks[] = [
75
+                    'type' => $this->detectAttackType($value),
76
+                    'param' => $key,
77
+                    'value' => $value
78
+                ];
79
+            }
80
+        }
81
+
82
+        // 检查请求头(可选)
83
+        if ($this->checkHeaders($request)) {
84
+            $attacks[] = [
85
+                'type' => 'header_injection',
86
+                'param' => 'headers',
87
+                'value' => json_encode($request->header(), JSON_UNESCAPED_UNICODE)
88
+            ];
89
+        }
90
+
91
+        return $attacks;
92
+    }
93
+
94
+    /**
95
+     * 检测攻击
96
+     */
97
+    protected function detectAttack($value, string $key = ''): bool
98
+    {
99
+        if (!is_string($value)) {
100
+            return false;
101
+        }
102
+
103
+        // 检查SQL注入
104
+        if ($this->detectSqlInjection($value)) {
105
+            return true;
106
+        }
107
+
108
+        // 检查XSS攻击
109
+        if ($this->detectXss($value)) {
110
+            return true;
111
+        }
112
+
113
+        // 检查命令注入
114
+        if ($this->detectCommandInjection($value)) {
115
+            return true;
116
+        }
117
+
118
+        // 检查路径遍历
119
+        if ($this->detectPathTraversal($value)) {
120
+            return true;
121
+        }
122
+
123
+        // 检查文件包含
124
+        if ($this->detectFileInclusion($value)) {
125
+            return true;
126
+        }
127
+
128
+        return false;
129
+    }
130
+
131
+    /**
132
+     * 检测SQL注入
133
+     */
134
+    protected function detectSqlInjection(string $input): bool
135
+    {
136
+        $patterns = [
137
+            // Union注入
138
+            '/union\s+(all\s+)?select/i',
139
+
140
+            // 注释符
141
+            '/--\s+|\/\*.*?\*\//s',
142
+
143
+            // 延时注入
144
+            '/sleep\s*\(\s*\d+\s*\)/i',
145
+            '/benchmark\s*\(.*?\)/i',
146
+            '/waitfor\s+delay/i',
147
+
148
+            // 条件语句
149
+            '/or\s+[\'"]?[\d\w][\'"]?\s*=\s*[\'"]?[\d\w][\'"]?/i',
150
+            '/and\s+[\'"]?[\d\w][\'"]?\s*=\s*[\'"]?[\d\w][\'"]?/i',
151
+
152
+            // 堆叠查询
153
+            '/;\s*(select|insert|update|delete|drop|create|alter|exec)/i',
154
+
155
+            // 错误注入
156
+            '/extractvalue\s*\(|updatexml\s*\(/i',
157
+
158
+            // 盲注
159
+            '/if\s*\(.*?,\s*.*?,\s*.*?\)/i',
160
+        ];
161
+
162
+        foreach ($patterns as $pattern) {
163
+            if (preg_match($pattern, $input)) {
164
+                return true;
165
+            }
166
+        }
167
+
168
+        return false;
169
+    }
170
+
171
+    /**
172
+     * 检测XSS攻击
173
+     */
174
+    protected function detectXss(string $input): bool
175
+    {
176
+        $patterns = [
177
+            // 脚本标签
178
+            '/<script\b[^>]*>(.*?)<\/script>/is',
179
+            '/javascript\s*:/i',
180
+            '/on(load|error|click|mouse|key|focus|blur|submit)\s*=/i',
181
+
182
+            // 事件处理
183
+            '/<\w+\s+[^>]*on\w+\s*=[^>]*>/i',
184
+
185
+            // 数据协议
186
+            '/data\s*:/i',
187
+            '/vbscript\s*:/i',
188
+
189
+            // 框架/对象
190
+            '/<iframe\b[^>]*>/i',
191
+            '/<object\b[^>]*>/i',
192
+            '/<embed\b[^>]*>/i',
193
+            '/<applet\b[^>]*>/i',
194
+        ];
195
+
196
+        foreach ($patterns as $pattern) {
197
+            if (preg_match($pattern, $input)) {
198
+                return true;
199
+            }
200
+        }
201
+
202
+        return false;
203
+    }
204
+
205
+    /**
206
+     * 检测命令注入
207
+     */
208
+    protected function detectCommandInjection(string $input): bool
209
+    {
210
+        $patterns = [
211
+            // 系统命令
212
+            '/(?:system|exec|shell_exec|passthru|proc_open|popen|pcntl_exec)\s*\(/i',
213
+
214
+            // 管道符
215
+            '/\|\s*\w+/',
216
+            '/;\s*\w+/',
217
+
218
+            // 危险命令
219
+            '/\b(?:rm\s+-|cat\s+\/|wget\s+|curl\s+|nc\s+)\b/i',
220
+
221
+            // 反引号
222
+            '/`.*?`/',
223
+        ];
224
+
225
+        foreach ($patterns as $pattern) {
226
+            if (preg_match($pattern, $input)) {
227
+                return true;
228
+            }
229
+        }
230
+
231
+        return false;
232
+    }
233
+
234
+    /**
235
+     * 检测路径遍历
236
+     */
237
+    protected function detectPathTraversal(string $input): bool
238
+    {
239
+        $patterns = [
240
+            '/\.\.\//',
241
+            '/\.\.\\\/',
242
+            '/\/etc\/passwd/i',
243
+            '/\/proc\/self/i',
244
+            '/\.\.%2f/i',
245
+            '/\.\.%5c/i',
246
+        ];
247
+
248
+        foreach ($patterns as $pattern) {
249
+            if (preg_match($pattern, $input)) {
250
+                return true;
251
+            }
252
+        }
253
+
254
+        return false;
255
+    }
256
+
257
+    /**
258
+     * 检测文件包含
259
+     */
260
+    protected function detectFileInclusion(string $input): bool
261
+    {
262
+        $patterns = [
263
+            '/include\s*\(|require\s*\(|include_once|require_once/i',
264
+            '/php:\/\/filter/i',
265
+            '/phar:\/\//i',
266
+            '/zip:\/\//i',
267
+            '/expect:\/\//i',
268
+        ];
269
+
270
+        foreach ($patterns as $pattern) {
271
+            if (preg_match($pattern, $input)) {
272
+                return true;
273
+            }
274
+        }
275
+
276
+        return false;
277
+    }
278
+
279
+    /**
280
+     * 检查请求头
281
+     */
282
+    protected function checkHeaders(Request $request): bool
283
+    {
284
+        $suspiciousHeaders = [
285
+            'x-forwarded-for' => '/^[\d\.\,]+$/',
286
+            'user-agent' => '', // 可以为空,但可以检查恶意UA
287
+            'referer' => '',    // 检查可疑referer
288
+        ];
289
+
290
+        foreach ($suspiciousHeaders as $header => $pattern) {
291
+            $value = $request->header($header);
292
+            if ($value && $pattern && !preg_match($pattern, $value)) {
293
+                return true;
294
+            }
295
+        }
296
+
297
+        return false;
298
+    }
299
+
300
+    /**
301
+     * 检测攻击类型
302
+     */
303
+    protected function detectAttackType(string $input): string
304
+    {
305
+        if ($this->detectSqlInjection($input)) {
306
+            return 'sql_injection';
307
+        }
308
+        if ($this->detectXss($input)) {
309
+            return 'xss';
310
+        }
311
+        if ($this->detectCommandInjection($input)) {
312
+            return 'command_injection';
313
+        }
314
+        if ($this->detectPathTraversal($input)) {
315
+            return 'path_traversal';
316
+        }
317
+        if ($this->detectFileInclusion($input)) {
318
+            return 'file_inclusion';
319
+        }
320
+
321
+        return 'unknown';
322
+    }
323
+
324
+    /**
325
+     * 处理攻击
326
+     */
327
+    protected function handleAttack(Request $request, array $attacks): void
328
+    {
329
+        $ip = $request->ip();
330
+
331
+        // 记录攻击日志
332
+        foreach ($attacks as $attack) {
333
+            $this->logAttack($ip, $request, $attack);
334
+        }
335
+
336
+        // 检查是否达到封禁阈值
337
+        if ($this->config['enable_blocking']) {
338
+            $this->checkAndBlockIp($ip);
339
+        }
340
+    }
341
+
342
+    /**
343
+     * 记录攻击日志到数据库
344
+     */
345
+    protected function logAttack(string $ip, Request $request, array $attack): void
346
+    {
347
+        if (!$this->config['enable_logging']) {
348
+            return;
349
+        }
350
+
351
+        try {
352
+            // 限制参数值长度
353
+            $paramValue = mb_substr($attack['value'], 0, $this->config['max_param_length'], 'UTF-8');
354
+
355
+            // 获取用户代理
356
+            $userAgent = $request->header('user-agent', '');
357
+
358
+            // 获取其他信息
359
+            $requestData = [
360
+                'method' => $request->method(),
361
+                'url' => $request->url(),
362
+                'full_url' => $request->url(true),
363
+                'get_params' => json_encode($request->get(), JSON_UNESCAPED_UNICODE),
364
+                'post_params' => json_encode($request->post(), JSON_UNESCAPED_UNICODE),
365
+            ];
366
+
367
+            // 使用参数绑定防止二次注入
368
+            Db::name('security_log')->insert([
369
+                'attack_type' => $attack['type'],
370
+                'ip_address' => $ip,
371
+                'param_name' => $attack['param'],
372
+                'param_value' => $paramValue,
373
+                'user_agent' => mb_substr($userAgent, 0, 500, 'UTF-8'),
374
+                'request_method' => $request->method(),
375
+                'request_url' => mb_substr($request->url(), 0, 500, 'UTF-8'),
376
+                'request_data' => json_encode($requestData, JSON_UNESCAPED_UNICODE),
377
+                'create_time' => date('Y-m-d H:i:s'),
378
+                'update_time' => date('Y-m-d H:i:s')
379
+            ]);
380
+
381
+            // 更新IP攻击计数
382
+            $this->incrementIpAttackCount($ip);
383
+
384
+        } catch (\Exception $e) {
385
+            // 数据库写入失败时记录到文件日志
386
+            Log::error('安全日志记录失败: ' . $e->getMessage());
387
+        }
388
+    }
389
+
390
+    /**
391
+     * 增加IP攻击计数
392
+     */
393
+    protected function incrementIpAttackCount(string $ip): void
394
+    {
395
+        $key = 'security:attack_count:' . $ip;
396
+        $count = Cache::inc($key, 1);
397
+
398
+        // 设置24小时过期
399
+        if ($count === 1) {
400
+            Cache::expire($key, 86400);
401
+        }
402
+    }
403
+
404
+    /**
405
+     * 检查并封禁IP
406
+     */
407
+    protected function checkAndBlockIp(string $ip): void
408
+    {
409
+        $key = 'security:attack_count:' . $ip;
410
+        $count = Cache::get($key, 0);
411
+
412
+        if ($count >= $this->config['attack_threshold']) {
413
+            $blockKey = 'security:blocked_ip:' . $ip;
414
+            Cache::set($blockKey, 1, $this->config['block_duration']);
415
+
416
+            // 记录封禁日志
417
+            Log::warning('IP被封禁', [
418
+                'ip' => $ip,
419
+                'attack_count' => $count,
420
+                'block_duration' => $this->config['block_duration']
421
+            ]);
422
+        }
423
+    }
424
+
425
+    /**
426
+     * 检查IP是否被封禁
427
+     */
428
+    protected function isIpBlocked(string $ip): bool
429
+    {
430
+        $key = 'security:blocked_ip:' . $ip;
431
+        return Cache::has($key);
432
+    }
433
+
434
+    /**
435
+     * 记录封禁访问
436
+     */
437
+    protected function logBlockedAccess(Request $request): void
438
+    {
439
+        Log::info('封禁IP访问被阻止', [
440
+            'ip' => $request->ip(),
441
+            'url' => $request->url(),
442
+            'method' => $request->method(),
443
+            'user_agent' => $request->header('user-agent', '')
444
+        ]);
445
+    }
446
+
447
+    /**
448
+     * 封禁响应
449
+     */
450
+    protected function blockResponse(): Response
451
+    {
452
+        return response()
453
+            ->json([
454
+                'code' => 403,
455
+                'message' => '访问被拒绝',
456
+                'data' => null
457
+            ])
458
+            ->code(403)
459
+            ->header([
460
+                'Content-Type' => 'application/json',
461
+                'Retry-After' => 3600
462
+            ]);
463
+    }
464
+
465
+    /**
466
+     * 错误响应
467
+     */
468
+    protected function errorResponse(): Response
469
+    {
470
+        return response()
471
+            ->json([
472
+                'code' => 400,
473
+                'message' => '请求参数错误',
474
+                'data' => null
475
+            ])
476
+            ->code(400);
477
+    }
478
+}

+ 6 - 0
config/middleware.php

@@ -1,9 +1,15 @@
1 1
 <?php
2 2
 // 中间件配置
3 3
 return [
4
+    // 全局中间件
5
+    '' => [
6
+        // ... 其他中间件
7
+        app\middleware\SecurityMiddleware::class,
8
+    ],
4 9
     // 别名或分组
5 10
     'alias'    => [
6 11
         'api_log' => \app\middleware\ApiRequestLogMiddleware::class,
12
+        'security' => \app\middleware\SecurityMiddleware::class,
7 13
     ],
8 14
     // 优先级设置,此数组中的中间件会按照数组中的顺序优先执行
9 15
     'priority' => [],