Webman 防 CC 攻击方案

Webman 防 CC 攻击方案

概述

防 CC 攻击分两层:边缘拦截(Nginx)+ 应用层检测(webman)。两层配合才能既挡得住、又误伤少。

整体架构

攻击流量
   ↓
[Nginx 限流]          ← 入口挡掉 80% 的高频请求
   ↓
[webman 中间件]       ← 行为分析 + 智能封禁
   ↓
[fail2ban]            ← 服务器层兜底,iptables 直接拉黑
   ↓
正常请求 → 业务

一、Nginx 层拦截

在 nginx 的 http 段加限流规则:

nginx
# 定义限流规则(按 IP)
limit_req_zone $binary_remote_addr zone=cc_limit:10m rate=10r/s;

# 定义并发连接限制
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

server {
    # ...

    # 全局限流:每秒 10 次请求,允许瞬时突发 20 个
    limit_req zone=cc_limit burst=20 nodelay;
    limit_req_status 503;

    # 限制单个 IP 并发连接数
    limit_conn conn_limit 20;
    limit_conn_status 503;
}

参数说明:

参数 含义
rate=10r/s 每秒允许 10 次请求
burst=20 允许瞬时突发 20 个请求进队列
nodelay 队列满则直接返回 503,不等待
limit_conn 20 单 IP 最大 20 个并发连接

注意: 如果有多台 webman 服务器,nginx 层的限流是分散在每台 nginx 上的。此时建议同时部署 webman 中间件做统一限流。


二、webman 全局限流中间件

2.1 安装限流组件

bash
composer require webman/limiter

2.2 创建 CC 防护中间件

php
// app/middleware/CcProtect.php
<?php
namespace app\middleware;

use support\Request;
use support\limiter\Limiter;
use support\limiter\RateLimitException;
use Webman\RedisQueue\Redis;

class CcProtect
{
    // 高频 IP 黑名单(Redis Set,TTL 10分钟)
    private const BLACKLIST_KEY = 'cc:blacklist';
    private const BAN_TTL = 600;  // 封禁 10 分钟

    // 记录请求的滑动窗口(用于行为分析)
    private const WINDOW_KEY = 'cc:window:';
    private const WINDOW_SIZE = 60;  // 60 秒窗口

    public function handle(Request $request, \Closure $next)
    {
        $ip = $this->getRealIp($request);

        // 1. 检查是否在黑名单(直接拦截)
        if ($this->isBlacklisted($ip)) {
            return response('Service Unavailable', 503);
        }

        // 2. 全局限流(每秒最多 100 次 / IP)
        try {
            Limiter::check($ip, 100, 1);
        } catch (RateLimitException $e) {
            $this->addToBlacklist($ip);
            return response('Service Unavailable', 503);
        }

        // 3. 行为检测(60秒内超过 500 次请求 → 加入黑名单)
        $this->checkBehavior($ip);

        // 4. 特殊路径限流
        $this->checkSpecialPaths($request, $ip);

        return $next($request);
    }

    private function getRealIp(Request $request): string
    {
        $headers = [
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_REAL_IP',
            'REMOTE_ADDR',
        ];
        foreach ($headers as $header) {
            if (!empty($_SERVER[$header])) {
                $ips = explode(',', $_SERVER[$header]);
                return trim($ips[0]);
            }
        }
        return '0.0.0.0';
    }

    private function isBlacklisted(string $ip): bool
    {
        return (bool) Redis::sIsMember(self::BLACKLIST_KEY, $ip);
    }

    private function addToBlacklist(string $ip): void
    {
        Redis::sAdd(self::BLACKLIST_KEY, $ip);
        Redis::expire(self::BLACKLIST_KEY, self::BAN_TTL);

        $log = sprintf(
            "[%s] CC攻击拦截 IP: %s\n",
            date('Y-m-d H:i:s'),
            $ip
        );
        @file_put_contents(runtime()->get('path') . '/logs/cc_attack.log', $log, FILE_APPEND);
    }

    private function checkBehavior(string $ip): void
    {
        $key = self::WINDOW_KEY . $ip;
        $count = Redis::incr($key);

        if ($count === 1) {
            Redis::expire($key, self::WINDOW_SIZE);
        }

        if ($count > 500) {
            $this->addToBlacklist($ip);
        }
    }

    private function checkSpecialPaths(Request $request, string $ip): void
    {
        $path = $request->path();

        $limits = [
            '/api/login'     => ['limit' => 5,  'ttl' => 60],      // 登录:1分钟5次
            '/api/register'  => ['limit' => 3,  'ttl' => 300],     // 注册:5分钟3次
            '/api/sms/send'  => ['limit' => 5,  'ttl' => 3600],    // 短信:1小时5次
            '/api/reset-pwd' => ['limit' => 3,  'ttl' => 1800],    // 找回密码:30分钟3次
        ];

        foreach ($limits as $pattern => $rule) {
            if (str_starts_with($path, $pattern)) {
                try {
                    Limiter::check($ip . ':' . $pattern, $rule['limit'], $rule['ttl']);
                } catch (RateLimitException $e) {
                    throw new RateLimitException("操作过于频繁,请稍后重试");
                }
            }
        }
    }
}

2.3 注册全局中间件

php
// config/middleware.php
return [
    '' => [
        app\middleware\CcProtect::class,
    ],
];

2.4 限流组件配置

php
// config/plugin/webman/limiter/app.php
return [
    'enable' => true,
    'driver' => 'redis',  // 生产环境用 redis
    'stores' => [
        'redis' => [
            'connection' => 'default',
        ]
    ],
    'ip_whitelist' => [
        '127.0.0.1',
    ],
];

三、fail2ban 服务器层(兜底)

当攻击流量非常大时,Nginx 和 webman 都可能被撑满。这时候用 fail2ban 在服务器层通过 iptables 直接拉黑 IP。

3.1 安装

bash
yum install fail2ban -y

3.2 创建规则配置

ini
# /etc/fail2ban/jail.d/cc-attack.conf
[nginx-cc]
enabled   = true
filter    = nginx-cc
action    = iptables-multiport[name=cc, port="http,https"]
logpath   = /www/wwwlogs/cc_attack.log
maxretry  = 3
findtime  = 60
bantime   = 600

3.3 创建 filter

ini
# /etc/fail2ban/filter.d/nginx-cc.conf
[Definition]
failregex = ^\[.*\] CC攻击拦截 IP: <HOST>
ignoreregex =

3.4 启动

bash
systemctl enable fail2ban
systemctl start fail2ban

四、参数调优建议

开发 / 测试阶段

参数 建议值
全局限流 100r/s
突发 burst 20
行为阈值 500次/60s
黑名单 TTL 10 分钟
登录限流 5次/60s

生产环境(按需调整)

先跑几天观察日志,再精细调参:

bash
# 查看 CC 攻击日志
tail -f /www/wwwlogs/cc_attack.log

根据实际流量调整行为阈值,别设太低,否则 VPN 用户、办公室共用出口 IP 会被误封。


五、其他建议

5.1 启用 nginx X-Forwarded-For

确保 nginx 配置正确传递真实 IP:

nginx
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;

5.2 CDN 层防护(如果有)

在 Nginx 层之前加一层 CDN(如 CloudFlare),CDN 本身就有 DDoS / CC 防护能力,可以挡掉大部分攻击。

5.3 日志分析

定期分析被拦截的 IP,判断是真实攻击还是误封:

bash
# 统计被拦截次数最多的 IP
awk '{print $NF}' /www/wwwlogs/cc_attack.log | sort | uniq -c | sort -rn | head -20

六、排查清单

  • [ ] Nginx 限流配置已添加
  • [ ] webman/limiter 已安装
  • [ ] CcProtect 中间件已注册为全局中间件
  • [ ] 限流驱动设为 redis
  • [ ] fail2ban 已配置并启动
  • [ ] 日志路径已创建且有写权限
  • [ ] 观察日志确认规则生效

评论 (0)

发表评论