SecurityMiddleware.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  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, $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. // 事件处理(注意:属性名必须是完整的 onXXX,避免匹配到属性值中的 actionArg 等)
  151. '/<\w+\s+[^>]*\bon\w+\s*=\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*\(\s*[\'\"\$\w]/i',
  176. // 管道符后跟命令(避免匹配单纯的|字符)
  177. '/\|\s*(?:rm\s|cat\s|ls\s|wget\s|curl\s|nc\s|sh\s|bash\s)/i',
  178. // 分号后跟命令(避免匹配HTML实体如&nbsp;)
  179. '/;\s*(?:rm\s|cat\s|ls\s|wget\s|curl\s|nc\s|sh\s|bash\s|\$\w+)/i',
  180. // 危险命令(更精确的匹配)
  181. '/\b(?:rm\s+-(?:rf?|i)\b|cat\s+\/\w|wget\s+http|curl\s+http|nc\s+-[lv])\b/i',
  182. // 反引号包裹命令(避免匹配代码块中的反引号)
  183. '/`\s*(?:rm\s|cat\s|ls\s|wget\s|curl\s|nc\s|sh\s|bash\s)/i',
  184. ];
  185. foreach ($patterns as $pattern) {
  186. if (preg_match($pattern, $input)) {
  187. return true;
  188. }
  189. }
  190. return false;
  191. }
  192. /**
  193. * 检测路径遍历
  194. */
  195. protected function detectPathTraversal(string $input): bool
  196. {
  197. $patterns = [
  198. '/\.\.\//',
  199. '/\.\.\\\/',
  200. '/\/etc\/passwd/i',
  201. '/\/proc\/self/i',
  202. '/\.\.%2f/i',
  203. '/\.\.%5c/i',
  204. ];
  205. foreach ($patterns as $pattern) {
  206. if (preg_match($pattern, $input)) {
  207. return true;
  208. }
  209. }
  210. return false;
  211. }
  212. /**
  213. * 检测文件包含
  214. */
  215. protected function detectFileInclusion(string $input): bool
  216. {
  217. $patterns = [
  218. '/include\s*\(|require\s*\(|include_once|require_once/i',
  219. '/php:\/\/filter/i',
  220. '/phar:\/\//i',
  221. '/zip:\/\//i',
  222. '/expect:\/\//i',
  223. ];
  224. foreach ($patterns as $pattern) {
  225. if (preg_match($pattern, $input)) {
  226. return true;
  227. }
  228. }
  229. return false;
  230. }
  231. /**
  232. * 检查请求头
  233. */
  234. protected function checkHeaders(Request $request): bool
  235. {
  236. $suspiciousHeaders = [
  237. 'x-forwarded-for' => '/^[\d\.\,]+$/',
  238. 'user-agent' => '', // 可以为空,但可以检查恶意UA
  239. 'referer' => '', // 检查可疑referer
  240. ];
  241. foreach ($suspiciousHeaders as $header => $pattern) {
  242. $value = $request->header($header);
  243. if ($value && $pattern && !preg_match($pattern, $value)) {
  244. return true;
  245. }
  246. }
  247. return false;
  248. }
  249. /**
  250. * 检测攻击类型
  251. */
  252. protected function detectAttackType(string $input): string
  253. {
  254. if ($this->detectSqlInjection($input)) {
  255. return 'sql_injection';
  256. }
  257. if ($this->detectXss($input)) {
  258. return 'xss';
  259. }
  260. if ($this->detectCommandInjection($input)) {
  261. return 'command_injection';
  262. }
  263. if ($this->detectPathTraversal($input)) {
  264. return 'path_traversal';
  265. }
  266. if ($this->detectFileInclusion($input)) {
  267. return 'file_inclusion';
  268. }
  269. return 'unknown';
  270. }
  271. /**
  272. * 处理攻击
  273. */
  274. protected function handleAttack(Request $request, array $attacks): void
  275. {
  276. $ip = $this->getRealIp($request);
  277. // 记录攻击日志
  278. foreach ($attacks as $attack) {
  279. $this->logAttack($ip, $request, $attack);
  280. }
  281. // 检查是否达到封禁阈值
  282. if ($this->config['enable_blocking']) {
  283. $this->checkAndBlockIp($ip);
  284. }
  285. }
  286. /**
  287. * 获取真实客户端IP(支持Swoole模式)
  288. */
  289. protected function getRealIp(Request $request): string
  290. {
  291. $ip = '0.0.0.0';
  292. // 1. 优先从Swoole的header中获取
  293. if ($this->isSwooleMode()) {
  294. // Swoole模式下,真实IP通常在x-real-ip或x-forwarded-for中
  295. $ip = $request->header('x-real-ip', '');
  296. if (empty($ip)) {
  297. // 如果有多个代理,x-forwarded-for是逗号分隔的列表,取第一个
  298. $xff = $request->header('x-forwarded-for', '');
  299. if (!empty($xff)) {
  300. $ips = explode(',', $xff);
  301. $ip = trim($ips[0]);
  302. }
  303. }
  304. }
  305. // 2. 如果Swoole模式获取失败,尝试常规方法
  306. if (empty($ip) || !filter_var($ip, FILTER_VALIDATE_IP)) {
  307. $ip = $request->ip();
  308. }
  309. // 3. 验证IP格式
  310. return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '0.0.0.0';
  311. }
  312. /**
  313. * 检查是否运行在Swoole模式
  314. */
  315. protected function isSwooleMode(): bool
  316. {
  317. return extension_loaded('swoole') &&
  318. defined('SWOOLE_VERSION') &&
  319. (php_sapi_name() === 'cli' || isset($_SERVER['SWOOLE_SERVER']));
  320. }
  321. /**
  322. * 记录攻击日志到数据库
  323. */
  324. protected function logAttack(string $ip, Request $request, array $attack): void
  325. {
  326. if (!$this->config['enable_logging']) {
  327. return;
  328. }
  329. try {
  330. // 限制参数值长度
  331. $paramValue = mb_substr($attack['value'], 0, $this->config['max_param_length'], 'UTF-8');
  332. // 获取用户代理
  333. $userAgent = $request->header('user-agent', '');
  334. // 获取其他信息
  335. $requestData = [
  336. 'method' => $request->method(),
  337. 'url' => $request->url(),
  338. 'full_url' => $request->url(true),
  339. 'get_params' => json_encode($request->get(), JSON_UNESCAPED_UNICODE),
  340. 'post_params' => json_encode($request->post(), JSON_UNESCAPED_UNICODE),
  341. ];
  342. // 使用参数绑定防止二次注入
  343. Db::name('security_log')->insert([
  344. 'attack_type' => $attack['type'],
  345. 'ip_address' => $ip,
  346. 'param_name' => $attack['param'],
  347. 'param_value' => $paramValue,
  348. 'user_agent' => mb_substr($userAgent, 0, 500, 'UTF-8'),
  349. 'request_method' => $request->method(),
  350. 'request_url' => mb_substr($request->url(), 0, 500, 'UTF-8'),
  351. 'request_data' => json_encode($requestData, JSON_UNESCAPED_UNICODE),
  352. 'create_time' => date('Y-m-d H:i:s'),
  353. 'update_time' => date('Y-m-d H:i:s')
  354. ]);
  355. // 更新IP攻击计数
  356. $this->incrementIpAttackCount($ip);
  357. } catch (\Exception $e) {
  358. // 数据库写入失败时记录到文件日志
  359. Log::error('安全日志记录失败: ' . $e->getMessage());
  360. }
  361. }
  362. /**
  363. * 增加IP攻击计数
  364. */
  365. protected function incrementIpAttackCount(string $ip): void
  366. {
  367. $key = 'security:attack_count:' . $ip;
  368. $count = Cache::inc($key, 1);
  369. // 设置24小时过期
  370. if ($count === 1) {
  371. Cache::expire($key, 86400);
  372. }
  373. }
  374. /**
  375. * 检查并封禁IP
  376. */
  377. protected function checkAndBlockIp(string $ip): void
  378. {
  379. $key = 'security:attack_count:' . $ip;
  380. $count = Cache::get($key, 0);
  381. if ($count >= $this->config['attack_threshold']) {
  382. $blockKey = 'security:blocked_ip:' . $ip;
  383. Cache::set($blockKey, 1, $this->config['block_duration']);
  384. // 记录封禁日志
  385. Log::warning('IP被封禁', [
  386. 'ip' => $ip,
  387. 'attack_count' => $count,
  388. 'block_duration' => $this->config['block_duration']
  389. ]);
  390. }
  391. }
  392. /**
  393. * 检查IP是否被封禁
  394. */
  395. protected function isIpBlocked(string $ip): bool
  396. {
  397. $key = 'security:blocked_ip:' . $ip;
  398. return Cache::has($key);
  399. }
  400. /**
  401. * 记录封禁访问
  402. */
  403. protected function logBlockedAccess(Request $request): void
  404. {
  405. Log::info('封禁IP访问被阻止', [
  406. 'ip' => $request->ip(),
  407. 'url' => $request->url(),
  408. 'method' => $request->method(),
  409. 'user_agent' => $request->header('user-agent', '')
  410. ]);
  411. }
  412. /**
  413. * 封禁响应
  414. */
  415. protected function blockResponse(): Response
  416. {
  417. return Response::create([
  418. 'code' => 403,
  419. 'msg' => '访问被拒绝'
  420. ], 'json')->code(403);
  421. }
  422. /**
  423. * 错误响应
  424. */
  425. protected function errorResponse(): Response
  426. {
  427. return Response::create([
  428. 'code' => 400,
  429. 'msg' => '请求参数错误'
  430. ], 'json')->code(400);
  431. }
  432. }