SecurityMiddleware.php 13 KB

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