七牛cdn一定要开流量预警,没有预警的情况下是欠费后才能知道,好像有欠费不透支策略不过cdn流量统计有延时,盗刷流量如图所示:

所有流量基本上都来自江苏,只刷特定一个图都高达几十GB,尽量不要把大图或者别的大的资源放到对象存储cdn加速。根据后台的统计分析,查看top100流量和次数的ip,如果是某些ip段的,直接整段封禁。查看刷的资源是什么,如果可以删除,就删除处理,然后在后台刷新预取文件刷新,刷新一下cdn缓存确保资源不能访问。一般刷cdn的也会刷网站首页,我安装了nginx openresty版本,可以使用lua脚本监控网站访问的ip,再根据ip段存入redis有序集合,同时存一份大于某个阈值ip的有序集合,可以监控异常ip访问。如下是监控到的ip段

根据ip段去网站日志查询访问记录,果然发现了一个刷流量ip段

可以在nginx和七牛双向拉黑处理
nginx拉黑配置如下
geo $block_ip { default 0; # CIDR网段封禁 "58.222.32.0/24" 1; "58.222.37.0/24" 1; "58.222.48.0/24" 1; "122.227.98.0/24" 1; "180.97.235.0/24" 1; "180.97.250.0/24" 1; "180.118.170.0/24" 1; "218.90.200.0/24" 1; "218.90.204.0/24" 1; "221.230.244.0/24" 1; "222.186.16.0/24" 1; "222.186.132.0/24" 1; "222.186.159.0/24" 1; "117.136.2.0/24" 1; "117.136.47.0/24" 1; } server{ if ($block_ip) { return 403; } location / { access_by_lua_file /www/server/nginx/lua/auto_block.lua; # 指定Lua脚本路径 } }
lua监控脚本如下
local redis = require "resty.redis" local red = redis:new() red:set_timeout(500) -- 连接到Redis服务器并选择db1 local ok, err = red:connect("127.0.0.1", 6379) if not ok then ngx.log(ngx.ERR, "Failed to connect to Redis: ", err) return end local res, err = red:auth("xxx", "xxx") if not res then ngx.log(ngx.ERR, "Failed to authenticate Redis: ", err) return end local ok, err = red:select(1) -- 使用db1 if not ok then ngx.log(ngx.ERR, "Failed to select database db1: ", err) end -- 获取客户端真实IP local function get_client_ip() local ip = ngx.var.remote_addr local xff = ngx.req.get_headers()["X-Forwarded-For"] if xff then ip = string.match(xff, '([^,]+)') end return ip end -- 正确提取/24网段的函数 local function get_ip_segment(ip) local segments = {} for segment in string.gmatch(ip, "%d+") do table.insert(segments, segment) end if #segments == 4 then return segments[1] .. "." .. segments[2] .. "." .. segments[3] .. ".0/24" end return "0.0.0.0/24" -- 默认值,防止错误 end local client_ip = get_client_ip() local segment = get_ip_segment(client_ip) -- 定义统计Key和过期时间(1小时=3600秒) local STAT_EXPIRE = 3600 local ip_count_key = "stat:ip:" .. client_ip local segment_count_key = "stat:segment:" .. segment -- 定义监控有序集合Key local monitored_ips_zset = "monitored:ips:zset" -- IP监控有序集合 local monitored_segments_zset = "monitored:segments:zset" -- IP段监控有序集合 -- 使用管道提高性能 red:init_pipeline() -- 统计IP访问次数(刷新过期时间) red:incr(ip_count_key) red:expire(ip_count_key, STAT_EXPIRE) -- 统计IP段访问次数(刷新过期时间) red:incr(segment_count_key) red:expire(segment_count_key, STAT_EXPIRE) -- 执行管道命令 local results, err = red:commit_pipeline() if not results then ngx.log(ngx.ERR, "Failed to commit pipeline: ", err) else local ip_count = results[1] local segment_count = results[3] ngx.log(ngx.INFO, "IP ", client_ip, " count: ", ip_count, " | Segment ", segment, " count: ", segment_count) -- 检查是否需要加入监控有序集合(1小时内超过100次) if ip_count > 100 then -- 使用管道批量操作 red:init_pipeline() -- 将IP加入有序集合,score=访问次数 red:zadd(monitored_ips_zset, ip_count, client_ip) -- 刷新有序集合过期时间(24小时) red:expire(monitored_ips_zset, 86400) -- 刷新计数Key过期时间 red:expire(ip_count_key, STAT_EXPIRE) local _, err = red:commit_pipeline() if err then ngx.log(ngx.ERR, "Failed to update IP zset: ", err) else ngx.log(ngx.WARN, "IP added to monitored zset (1h>100): ", client_ip, " with score: ", ip_count) end end if segment_count > 100 then -- 使用管道批量操作 red:init_pipeline() -- 将IP段加入有序集合,score=访问次数 red:zadd(monitored_segments_zset, segment_count, segment) -- 刷新有序集合过期时间(24小时) red:expire(monitored_segments_zset, 86400) -- 刷新计数Key过期时间 red:expire(segment_count_key, STAT_EXPIRE) local _, err = red:commit_pipeline() if err then ngx.log(ngx.ERR, "Failed to update segment zset: ", err) else ngx.log(ngx.WARN, "Segment added to monitored zset (1h>100): ", segment, " with score: ", segment_count) end end end -- 保持原有连接池设置 red:set_keepalive(10000, 100)
还可以根据预警短信及时获取预警信息,根据自己的业务做拉黑处理,这里贴出七牛php sdk未收录的几个api,七牛官方通知支持短信和钉钉、飞书、企业微信的webhook。不过七牛的设置黑名单有一些问题,api提交以后,返回的是成功信息,这个成功信息是七牛放到队列成功不是配置成功,域名信息那里会显示配置中,但是千万不要以为会成功,有可能是失败的,所以即便cdn预警能hook到服务器,也可能黑名单不能及时配置成功,最终这个自动拉黑没有能实现,而且遇到问题工单也不能及时响应,所以不太建议使用七牛cdn。
<?php require_once __DIR__ . '/vendor/autoload.php'; use Qiniu\Auth; use Qiniu\Http\Error; use Qiniu\Http\Client; use Dotenv\Dotenv; /** * 获取某个cdn域名前100的访问 */ class CdnManagerv2 { private $auth; private $server; private $proxy; public function __construct(Auth $auth) { $this->auth = $auth; $this->server = 'https://fusion.qiniuapi.com'; } /** * https://developer.qiniu.com/fusion/4081/cdn-log-analysis#11 */ public function getTopCountIp($domain) { $req = array(); $req['domains'] = [$domain]; $req['region'] = 'global'; $req['startDate'] = date("Y-m-d",time()-86400); $req['endDate'] = date("Y-m-d"); $url = $this->server . '/v2/tune/loganalyze/topcountip'; $body = json_encode($req); return $this->post($url, $body); } /** * https://developer.qiniu.com/fusion/4081/cdn-log-analysis#12 */ public function getTopTrafficIp($domain) { $req = array(); $req['domains'] = [$domain]; $req['region'] = 'global'; $req['startDate'] = date("Y-m-d",time()-86400); $req['endDate'] = date("Y-m-d"); $url = $this->server . '/v2/tune/loganalyze/toptrafficip'; $body = json_encode($req); return $this->post($url, $body); } private function post($url, $body) { $headers = $this->auth->authorization($url, $body, 'application/json'); $headers['Content-Type'] = 'application/json'; $ret = Client::post($url, $body, $headers, null); if (!$ret->ok()) { return array(null, new Error($url, $ret)); } $r = ($ret->body === null) ? array() : $ret->json(); return array($r, null); } } /** * 获取域名信息和设置黑名单 */ class DomainManager { private $auth; private $server; public function __construct(Auth $auth) { $this->auth = $auth; $this->server = 'https://api.qiniu.com'; } /** * https://developer.qiniu.com/fusion/4246/the-domain-name#10 */ public function getDomainInfo($domain) { $url = $this->server . "/domain/".$domain; return $this->get($url,''); } /** * https://developer.qiniu.com/fusion/4246/the-domain-name#16 */ public function putIpacl($domain,$ips) { $url = $this->server . "/domain/".$domain."/ipacl"; $body = ['IPACL'=>['ipACLType'=>"black","ipACLValues"=>$ips]]; $body = json_encode($body); $headers = $this->auth->authorization($url,$body, 'application/json'); $headers['Content-Type'] = 'application/json'; $ret = Client::PUT($url,$body,$headers); if (!$ret->ok()) { return array(null, new Error($url, $ret)); } $r = ($ret->body === null) ? array() : $ret->json(); return array($r, null); } private function get($url,$body) { $headers = $this->auth->authorization($url,$body, 'application/x-www-form-urlencoded'); $headers['Content-Type'] = 'application/x-www-form-urlencoded'; $ret = Client::get($url, $headers); if (!$ret->ok()) { return array(null, new Error($url, $ret)); } $r = ($ret->body === null) ? array() : $ret->json(); return array($r, null); } } $dotenv = Dotenv::createImmutable(__DIR__); $dotenv->load(); $accessKey = getenv('QINIU_ACCESS_KEY'); $secretKey = getenv('QINIU_SECRET_KEY'); $cdn_domain = getenv('CDN_DOMAIN'); $auth = new Auth($accessKey, $secretKey); $domainManager = new DomainManager($auth); $info = $domainManager->getDomainInfo($cdn_domain); $ipACLValues = $info[0]['ipACL']['ipACLValues']; $ipACLValues = require_once "blackips.php"; $put_ret = $domainManager->putIpacl($cdn_domain,$ipACLValues); $cdn_manager_v2 = new CdnManagerv2($auth); $cdn_count = $cdn_manager_v2->getTopCountIp($cdn_domain); $cdn_data = $cdn_count[0]['data']; $cdn_data_com = array_combine($cdn_data['ips'],$cdn_data['count']); $cdn_trafic = $cdn_manager_v2->getTopTrafficIp($cdn_domain); $cdn_trafic_data = $cdn_trafic[0]['data']; $cdn_trafic_com = array_combine($cdn_trafic_data['ips'],$cdn_trafic_data['traffic']); $ip2region = new \Ip2Region('file',__DIR__.'/vendor/zoujingli/ip2region/db/ip2region_v4.xdb'); foreach($cdn_data_com as $key => $value){ echo mb_convert_encoding($ip2region->simple($key),'GBK', 'UTF-8').$key."<br/>"; // 中国广东省中山市【电信】 }