前端仔的一次运维实践

作者:yuanyxh日期:2025/10/25

最近有反馈官网访问异常卡顿,查看 Portainer 面板发现官网后台的后端服务所依赖的 jdk、mysql 等 docker 容器在不断的奔溃重建,查询发现奔溃日志,交由后端排查。

后端反馈服务器磁盘不足导致 mysql 无法启动,后端访问连不上数据库导致的。

查询磁盘占用,发现官网的 nginx 日志文件占用近 20GB,删除后官网后端访问正常运行。

日志切分与压缩

为了避免日志持续增长占用磁盘空间,需要对日志进行管理,这里使用 linux 系统自带的 logrotate 日志管理工具实现自动切割、压缩与清理。

创建 logrotate 配置文件:

1vim /etc/logrotate.d/nginx
2

写入特定配置:

1path/to/log/*.log {
2    size 500M                 # 达到指定大小时轮转一次
3    missingok                 # 日志文件缺失时不报错
4    rotate 14                 # 保留最近 14 个日志文件(约 2 周)
5    compress                  # 压缩旧日志(gzip 格式)
6    delaycompress             # 延迟压缩(下次轮转时压缩上一次日志,避免影响当前日志读写)
7    notifempty                # 日志为空时不轮转
8    create 0644 root root     # 新建日志文件的权限和属主
9    sharedscripts
10    postrotate                # 轮转后执行的命令,通知Nginx重新打开日志
11        # 检查宝塔环境 Nginx 的 PID 文件是否存在且可读
12        if [ -f /www/server/nginx/logs/nginx.pid ] && [ -r /www/server/nginx/logs/nginx.pid ]; then
13            # 向正确的 Nginx 实例发送 USR1 信号
14            kill -USR1 $(cat /www/server/nginx/logs/nginx.pid) >/dev/null 2>&1 || true
15        else
16            # 记录警告日志(可选,需 root 权限)
17            logger -t logrotate "宝塔 Nginx PID 文件缺失,日志切割可能失败"
18        fi
19    endscript
20}
21

手动触发一次以查看效果:

1logrotate -f /etc/logrotate.d/nginx  # -f 强制执行
2

排查日志

对日志进行管理后,官网访问仍旧缓慢,查看 nginx 日志发现日志增长飞速,且服务器监控面板显示外网出带宽使用率每天 24 小时都在 85% 及以上:

外网出带宽使用率

猜测可能遭遇流量攻击,在 nginx 配置中添加多项简易的代码,校验请求是否异常,其中下列代码可以拦截大部分异常请求,外网出带宽使用率回落至正常状态:

1if ($http_referer = "") {
2    return 403;
3}
4

Referer 记录请求的来源地址,浏览器一般会在请求中自动加上 Referer 头,通常伪装浏览器请求时都会伪造此请求头,但此次的攻击者显然是忽略。

话虽如此,我们却无法仅通过是否存在 referer 头来判断请求是否正常,因为浏览器刷新时、从书签访问时都不会携带此请求头,且为了隐私考虑,加载外部资源(图片、js、css)时可以明确禁止发送 referer,如果一刀切的话,官网运行会收到影响。

封禁固定 IP

为了官网尽快恢复访问,需要一个临时解决方案,分析 nginx 日志后,决定采用简易的策略记录查找异常 IP 并封禁,nginx 日志格式如下:

1IP - - [TIME] "Request_Method URI HTTP_Version" Status ContentLength "-" "User-Agent"
2

注意需要有足够数量的日志,能够反映出异常 IP,这里的日志是 2-3 天的,产生了约 3GB 的日志文件,约 1250 万次请求。

策略:

  1. 以 IP + URI + User-Agent 为 key,记录出现的次数
  2. 由于设置了浏览器缓存,正常访问不应该在这么短的时间范围内多次重复请求
  3. 针对 IP + User-Agent 判断不同 URI 多次请求的次数,大于阈值时判定异常

最终筛选出来的 IP 通过 nginx 配置封禁,这里筛选出了一万多个 IP。

观察筛选后的结果,发现很多 IP 的网段(115.115.2.89, 115.115.2.90)相同,可以归属于同一组下;优化筛选逻辑:上一步筛选过的结果进行二次筛选,提取 IP 的网段,得到 xx.xx.0.0/16 的结果(115.115.0.0/16),将这一个网段下的所有请求封禁,这里分组后只剩下 100 多组。

动态限流(尝试)

封禁异常 IP 网段后,服务器流量基本正常,但此非长久之计,不能保证后续不会有新的 IP 攻击出现,且封禁网段的做法有概率影响到正常用户。

为了长久发展,需要实现动态限流策略;使用 nginx 模块 limit_reqlimit_conn 限制特定时间内的请求数量与单 IP 并发数量:

1http {
2    # ① 请求频率限制:10M 内存存储 IP 状态,限制每秒 10 个请求
3    limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=10r/s;
4    # ② 并发连接限制:10M 内存存储 IP 连接数
5    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
6    
7    server {
8        listen 80;
9        server_name example.com;
10        
11        location / {
12            # 应用请求频率限制:允许突发 5 个请求,超出立即返回 429
13            limit_req zone=ip_limit burst=5 nodelay;
14            # 限制单 IP 最大并发连接数为 10
15            limit_conn conn_limit 10;
16            
17            # 自定义限流响应状态码(默认 503,推荐 429 Too Many Requests)
18            limit_req_status 429;
19            limit_conn_status 429;
20            
21            proxy_pass http://backend;
22        }
23        
24        # 自定义 429 错误页面
25        error_page 429 /429.html;
26        location = /429.html {
27            root /var/www/html;
28            internal;  # 仅内部访问
29        }
30    }
31}
32

实际测试效果不佳,原因:官网服务器带宽较小(5Mbps),设置较大的连接数与并发数时,起不到限制效果;设置较小的连接数与并发数时,会影响正常用户的访问,因为无缓存加载网页时会请求较多的 js、css、图片资源。

动态限流-策略 1

为了更精准的识别异常 IP,需要更灵活的逻辑控制,可以使用添加过 lua-nginx-module 模块的 nginx,通过组合 nginx + lua(逻辑控制) + redis(缓存) 实现复杂限流策略。

策略 1:

  1. IP + URI + User-Agent 为 key,记录访问次数
  2. 由于设置了浏览器缓存,正常访问不应该在这么短的时间范围内多次重复请求
  3. 如果 key 对应的访问次数大于阈值,直接返回 429
1http {
2    # 加载 Lua 模块
3    lua_package_path "/path/to/lua/?.lua;;";
4    
5    server {
6        listen 80;
7        server_name yourdomain.com;
8
9        # 首页及动态内容
10        location / {
11            access_by_lua_file /etc/nginx/lua/limit.lua;
12            proxy_pass http://backend;
13        }
14    }
15}
16
1local redis = require "resty.redis"
2local red = redis:new()
3red:set_timeout(1000)  -- 1 秒超时
4
5-- 连接 Redis(替换为实际地址)
6local ok, err = red:connect("127.0.0.1", 6379)
7if not ok then
8    ngx.log(ngx.ERR, "Redis 连接失败: ", err)
9    return ngx.exit(500)
10end
11
12-- 构建复合键:IP + User-Agent + URI
13local ip = ngx.var.binary_remote_addr
14local ua = ngx.var.http_user_agent or "unknown"
15local uri = ngx.var.uri
16local key = "req:" .. ip .. ":" .. ngx.md5(ua) .. ":" .. uri
17
18-- 10 秒内最多允许 2 次请求(缓存有效期内重复请求视为异常)
19local limit = 2
20local expire = 10
21
22-- Redis 原子操作:计数 + 过期时间设置
23local res, err = red:eval([[
24    local count = redis.call('incr', KEYS[1])
25    if count == 1 then
26        redis.call('expire', KEYS[1], ARGV[2])
27    end
28    return count
29]], 1, key, limit, expire)
30
31-- 判断是否超限
32if res and tonumber(res) > limit then
33    ngx.log(ngx.WARN, "触发限流: ", key)
34    return ngx.exit(429)  -- 返回 429 Too Many Requests
35end
36
37-- 保持 Redis 连接池
38red:set_keepalive(10000, 100)  -- 空闲超时 10 秒,池大小 100
39

效果不佳,问题在于对于每个 IP 不同的 URI 默认允许访问一次,在大批量的 IP 流量攻击下,外网出带宽使用率仍旧巨高不下。

动态限流-策略 2

针对上述痛点,思考出了以 Cookie 标识正常用户的策略:

  1. 接收到访问时,查询 IP + User-Agent 的 key 是否在 redis 中
  2. 如果存在,要求此次请求必须包含指定的 cookie,如果不包含,直接 403
  3. 如果不存在,生成 cookie 并 set-cookie,同时将 cookie 缓存至 redis
1local redis = require "resty.redis"
2local str = require "resty.string"
3local hmac = require "resty.hmac"
4
5-- 配置参数
6local config = {
7    redis_host = "127.0.0.1",
8    redis_port = 6379,
9    expire_seconds = 518400,  -- 6 天 = 6 * 24 * 3600 秒
10  	token = "xss_t90",
11  	pool_size = 100,
12    idle_timeout = 30000
13}
14
15-- 获取客户端真实 IP
16local function get_client_ip()
17    local ip = ngx.req.get_headers()["X-Real-IP"]
18
19    if not ip then
20        ip = ngx.req.get_headers()["X-Forwarded-For"]
21    end
22
23    if not ip then
24        ip = ngx.var.remote_addr
25    end
26    
27    return ip or "unknown"
28end
29
30-- 连接 Redis 并设置连接池
31local function connect_redis()
32    local red = redis:new()
33
34    red:set_timeout(1000)  -- 1 秒超时
35  
36    
37    local ok, err = red:connect(config.redis_host, config.redis_port)
38    if not ok then
39        ngx.log(ngx.ERR, "Redis 连接失败: ", err)
40        return nil, err
41    end
42  
43    -- 复用连接池
44    local count, err = red:get_reused_times()
45    if 0 == count then
46        -- 新连接需认证(如有密码)
47        -- red:auth("your_redis_password")
48    elseif err then
49        ngx.log(ngx.ERR, "Redis get reused times failed: ", err)
50    end
51    
52    return red
53end
54
55-- 生成加密令牌
56local function generate_token()
57  	local client_ip = get_client_ip()
58    local user_agent = ngx.var.http_user_agent or "unknown"
59    local secret = config.token
60    local timestamp = ngx.time()
61    local nonce = math.random(100000, 999999)  -- 6 位随机数
62    local plaintext = client_ip .. user_agent .. timestamp .. nonce
63  
64    -- HMAC-SHA256 加密
65    local hmac_obj = hmac:new(secret, hmac.ALGOS.SHA256)
66    hmac_obj:update(plaintext)
67  
68    return str.to_hex(hmac_obj:final()), timestamp
69end
70
71-- 主逻辑
72local function main()
73    -- 1. 获取请求标识要素
74    local client_ip = get_client_ip()
75    local user_agent = ngx.var.http_user_agent or "unknown"
76    
77    -- 2. 生成唯一键(IP + UA 哈希)
78  	local redis_key = "os_visit:" .. client_ip .. ":" .. ngx.md5(user_agent)  -- 用 MD5 哈希 UA 避免过长
79    
80    -- 4. 连接 Redis 并获取计数
81    local red = connect_redis()
82    if not red then
83        return -- Redis 故障时放行,避免影响正常访问
84    end
85    
86    local stored_token, err = red:get(redis_key)
87    if err then
88        ngx.log(ngx.ERR, "Redis get stored_token failed: ", err)
89        red:set_keepalive(config.idle_timeout, config.pool_size)
90        return
91    end
92    
93    -- 2. 无存储令牌:生成新令牌并设置 Cookie
94    if stored_token == ngx.null then
95        local token, timestamp = generate_token()
96
97        -- 存储令牌到 Redis
98        red:setex(redis_key, config.expire_seconds, token)
99
100        -- 设置 Cookie (HttpOnly + Secure,生产环境建议开启 Secure)
101        ngx.header["Set-Cookie"] = string.format(
102            "os_visit_token=%s; HttpOnly; SameSite=Strict; Path=/; Max-Age=%d",
103            token,
104      		config.expire_seconds
105        )
106
107        red:set_keepalive(config.idle_timeout, config.pool_size)
108
109        return
110    end
111
112    red:set_keepalive(config.idle_timeout, config.pool_size)  -- 提前归还连接池
113  
114  	local client_token = ngx.var.cookie_os_visit_token
115    
116    if not client_token then
117        
118        ngx.log(ngx.ERR, "异常用户, Cookie 不存在")
119
120        return ngx.exit(403)
121    
122    elseif client_token ~= stored_token then
123        ngx.log(ngx.ERR, "异常用户, Cookie 不匹配")
124    
125        -- Cookie 不匹配:拒绝访问
126        ngx.exit(403)
127    end
128    
129    -- 7. 正常请求,继续处理
130end
131
132-- 执行主逻辑
133main()
134

预期的结果是攻击方没有处理 cookie 携带的逻辑,当 IP 首次访问时设置 Cookie,后续资源请求未携带 cookie 而被拦截;实际结果是 Cookie 过于常见,大部分工具都会自动处理他。

动态限流-策略 3

提出人机验证的想法,要求用户访问时进行真人验证,通过则允许访问,未实践,因为用户体验较差。

动态限流-策略 4

观察日志,发现异常 IP 都使用 HTTP/1.1 的协议版本,而正常用户使用的现代浏览器基本使用 HTTP/2.0 协议版本,基于此,实现多级校验策略:

  1. 如果 HTTP 协议版本大于等于 2.0, 或 Referer 请求头不为空,不做后续步骤直接放行
  2. 针对 HTTP 协议版本小于 2.0, 或 Referer 请求头为空的,执行下列逻辑
  3. 判断当前 IP 是否存在于白名单列表,存在直接放行,反之执行下列逻辑
  4. 提前当前 IP 的网段,(115.115.2.80 -> 115.115),查询此网段下的 IP 数量,大于阈值时,重定向至人机校验页面,小于阈值时允许访问
  5. 将此 IP 添加到网段集合

人机校验逻辑:

  1. 生成 4 位随机字符串,缓存至 redis,key = IP + User-Agent
  2. 根据字符串生成图片,展示在前端页面
  3. 用户输入并提交
  4. 验证是否通过,通过则将此 IP + User-Agent 添加进白名单中,设置较长的过期时间

此逻辑的优点:

  • 根据现有日志分析,明确正常用户的特征,减少正常用户的操作步骤
  • 基于网段分组的封禁,网段组内 IP 大于阈值时,后续新的 IP 进来直接重定向至人机校验,较少了服务器压力
  • 使用人机校验添加兜底处理,避免正常用户被误封,且校验通过后较长一段时间内白名单通行,避免了多次校验

完整代码:

1http {
2    server {
3        lua_package_path "/www/server/nginx/lib/lua/?.lua;;";
4        lua_package_cpath "/www/server/nginx/lib/lua/?.so;;";
5
6        # 代理与校验
7        location / {
8            access_by_lua_file /usr/local/openresty/nginx/lua/auth.lua;
9            proxy_pass http://xxxx:xx;
10        }
11        
12        # 验证码图片生成接口
13        location /captcha-image {
14            content_by_lua_file /usr/local/openresty/nginx/lua/generate_captcha.lua;
15        }
16        
17        # 验证码验证接口
18        location /captcha-verify {
19            content_by_lua_file /usr/local/openresty/nginx/lua/verify_captcha.lua;
20        }
21        
22        # 验证码展示页面
23        location = /captcha {
24            alias /www/server/nginx/html/verify.html;
25            default_type text/html;
26            charset utf-8;
27            expires -1;
28            add_header Cache-Control "no-store, no-cache, must-revalidate";
29        }
30    }
31}
32

auth.lua:

1local redis = require "resty.redis"
2
3-- 配置参数(可根据业务调整)
4local config = {
5    redis_host = "127.0.0.1",
6    redis_port = 6379,
7    whitelist_key = "os_whitelist",
8    subnet_threshold = 2,
9    subnet_window = 1209600           -- 14 天
10}
11
12-- Redis 连接池管理
13local function close_redis(red)
14    if not red then return end
15    
16    local ok, err = red:set_keepalive(10000, 100)  -- 10 秒空闲超时,池大小 100
17    if not ok then
18        ngx.log(ngx.NOTICE, "Redis keepalive error: ", err)
19        red:close()
20    end
21end
22
23-- 获取客户端真实 IP(支持代理场景)
24local function get_client_ip()
25    local ip = ngx.req.get_headers()["X-Real-IP"]
26    
27    if not ip then
28        ip = ngx.req.get_headers()["X-Forwarded-For"] or ngx.var.remote_addr
29    end
30    
31    return ip:match("^[%d%.]+")  -- 提取首个 IP(避免代理链干扰)
32end
33
34-- 连接 Redis 并设置连接池
35local function connect_redis()
36    local red = redis:new()
37    red:set_timeout(1000)  -- 1 秒超时
38  
39    local ok, err = red:connect(config.redis_host, config.redis_port)
40    
41    if not ok then
42        ngx.log(ngx.NOTICE, "Redis 连接失败: ", err)
43        return nil, err
44    end
45  
46    -- 复用连接池
47    local count, err = red:get_reused_times()
48    
49    if count == 0 then
50        -- ngx.log(ngx.NOTICE, "Redis 无法连接, 需要密码")
51    elseif err then
52        ngx.log(ngx.NOTICE, "Redis 复用连接失败: ", err)
53    end
54    
55    return red
56end
57
58-- 主逻辑
59local function main()
60    local red = connect_redis()
61    
62    if not red then
63        return -- Redis 故障时放行,避免影响正常访问
64    end
65  
66    -- 1. 协议与 Referer 校验
67    local http_version = ngx.req.http_version()
68    local referer = ngx.var.http_referer or ""
69    
70    if http_version >= 2.0 or referer ~= "" then -- and & or
71        ngx.log(ngx.NOTICE, "HTTP 版本大于等于 2 或 referer 不为空, 默认放行, ", client_ip)
72        
73        return close_redis(red)  -- 直接放行
74    end
75  
76    -- 1. 白名单检查(最高优先级)
77    local client_ip = get_client_ip()
78    local user_agent = ngx.var.http_user_agent or "unknown"
79    local white_key = config.whitelist_key .. ":" .. client_ip .. "_" .. ngx.md5(user_agent)
80    local is_white, err = red:get(white_key)
81  
82    if is_white and is_white ~= ngx.null then
83        return close_redis(red) -- 白名单直接放行
84    end
85  
86    -- 3. 网段级访问控制(提取 IP 前两段)
87    local ip_segments = {}
88  
89    for seg in string.gmatch(client_ip, "%d+") do
90        table.insert(ip_segments, seg)
91    end
92  
93    local subnet_key = "os_visit:" .. ip_segments[1] .. "." .. ip_segments[2]
94    local full_ip_key = subnet_key .. ":" .. client_ip
95  
96    -- 3.1 检查网段 IP 数量
97    local subnet_ip_count, err = red:scard(subnet_key)  -- SCARD 获取集合元素数
98  
99    -- 3.2 添加新 IP 到网段集合
100    local is_new_ip, err = red:sadd(subnet_key, client_ip)  -- SADD 返回 1 表示新添加
101    if is_new_ip == 1 then
102        red:expire(subnet_key, config.subnet_window)  -- 重置网段过期时间
103    end
104  
105    if subnet_ip_count and subnet_ip_count >= config.subnet_threshold then
106        -- 网段超限,触发人机验证
107        close_redis(red)
108        return ngx.redirect("/captcha")
109    end
110    
111    close_redis(red)
112end
113
114-- 执行主逻辑
115main()
116

verify_captcha.lua:

1local redis = require "resty.redis"
2
3local config = {
4    redis_host = "127.0.0.1",
5    redis_port = 6379,
6    whitelist_expire = 1036800,         -- 白名单有效期(12 天)
7    whitelist_key = "os_whitelist",
8    auth_key = "os_auth"
9}
10
11-- Redis 连接池管理
12local function close_redis(red)
13    if not red then return end
14    
15    local ok, err = red:set_keepalive(10000, 100)  -- 10 秒空闲超时,池大小 100
16    if not ok then
17        ngx.log(ngx.NOTICE, "Redis keepalive error: ", err)
18        red:close()
19    end
20end
21
22-- 获取客户端真实 IP(支持代理场景)
23local function get_client_ip()
24    local ip = ngx.req.get_headers()["X-Real-IP"]
25    
26    if not ip then
27        ip = ngx.req.get_headers()["X-Forwarded-For"] or ngx.var.remote_addr
28    end
29    
30    return ip:match("^[%d%.]+")  -- 提取首个 IP(避免代理链干扰)
31end
32
33-- 连接 Redis 并设置连接池
34local function connect_redis()
35    local red = redis:new()
36    red:set_timeout(1000)  -- 1 秒超时
37  
38    local ok, err = red:connect(config.redis_host, config.redis_port)
39    
40    if not ok then
41        ngx.log(ngx.NOTICE, "Redis 连接失败: ", err)
42        return nil, err
43    end
44  
45    -- 复用连接池
46    local count, err = red:get_reused_times()
47    
48    if count == 0 then
49        -- ngx.log(ngx.NOTICE, "Redis 无法连接, 需要密码")
50    elseif err then
51        ngx.log(ngx.NOTICE, "Redis 复用连接失败: ", err)
52    end
53    
54    return red
55end
56
57-- 获取请求参数
58local args = ngx.req.get_uri_args()
59local authCode = args.code or ""
60
61if authCode == "" then
62    ngx.status = 400
63    ngx.say('{"status": "error", "msg": "Missing parameters"}')
64    return
65end
66
67-- 主逻辑
68local red = connect_redis()
69    
70if not red then
71    ngx.status = 500
72    ngx.say('{"status": "error", "msg": "Service exception, please try again later"}')
73  
74    return
75end
76
77-- 生成唯一标识与验证码
78local client_ip = get_client_ip()
79local user_agent = ngx.var.http_user_agent or "unknown"
80local redis_key = config.auth_key .. ":" .. client_ip .. "_" .. ngx.md5(user_agent)
81local white_key = config.whitelist_key .. ":" .. client_ip .. "_" .. ngx.md5(user_agent)
82
83local is_white, err = red:get(white_key)
84
85if is_white and is_white ~= ngx.null then -- 白名单禁止多次验证
86    return ngx.exit(403)
87end
88
89-- 验证验证码
90local stored_code, err = red:get(redis_key)
91
92if stored_code and stored_code ~= ngx.null then
93    -- 不区分大小写验证(增强用户体验)
94    if string.upper(authCode) == string.upper(stored_code) then
95        red:del(redis_key)  -- 验证通过后删除缓存
96    
97        local white_key = config.whitelist_key .. ":" .. client_ip .. "_" .. ngx.md5(user_agent)
98        red:setex(white_key, config.whitelist_expire, "1")
99    
100        ngx.say('{"status": "success", "msg": "OK"}')
101    else
102        ngx.say('{"status": "error", "msg": "Incorrect captcha"}')
103    end
104else
105    ngx.say('{"status": "error", "msg": "Captcha expired or invalid"}')
106end
107
108close_redis(red)
109

generate_captcha.lua:

1local gd = require "gd"
2local redis = require "resty.redis"
3local str = require "resty.string"
4
5-- 配置参数
6local config = {
7    width = 200,              -- 图像宽度
8    height = 120,             -- 图像高度
9    length = 4,               -- 验证码长度
10    redis_host = "127.0.0.1",
11    redis_port = 6379,
12    whitelist_key = "os_whitelist",
13    auth_key = "os_auth",
14    expire = 600
15}
16
17-- Redis 连接池管理
18local function close_redis(red)
19    if not red then return end
20    
21    local ok, err = red:set_keepalive(10000, 100)  -- 10 秒空闲超时,池大小 100
22    if not ok then
23        ngx.log(ngx.NOTICE, "Redis keepalive error: ", err)
24        red:close()
25    end
26end
27
28-- 获取客户端真实 IP(支持代理场景)
29local function get_client_ip()
30    local ip = ngx.req.get_headers()["X-Real-IP"]
31    
32    if not ip then
33        ip = ngx.req.get_headers()["X-Forwarded-For"] or ngx.var.remote_addr
34    end
35    
36    return ip:match("^[%d%.]+")  -- 提取首个 IP(避免代理链干扰)
37end
38
39-- 连接 Redis 并设置连接池
40local function connect_redis()
41    local red = redis:new()
42    red:set_timeout(1000)  -- 1 秒超时
43  
44    local ok, err = red:connect(config.redis_host, config.redis_port)
45    
46    if not ok then
47        ngx.log(ngx.NOTICE, "Redis 连接失败: ", err)
48        return nil, err
49    end
50  
51    -- 复用连接池
52    local count, err = red:get_reused_times()
53    
54    if count == 0 then
55        -- ngx.log(ngx.NOTICE, "Redis 无法连接, 需要密码")
56    elseif err then
57        ngx.log(ngx.NOTICE, "Redis 复用连接失败: ", err)
58    end
59    
60    return red
61end
62
63-- 生成随机验证码字符串
64local function generate_code()
65    local chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz0123456789"
66    local code = ""
67  
68    math.randomseed(os.time() + ngx.worker.pid())
69  
70    for _ = 1, config.length do
71        local idx = math.random(1, #chars)
72        code = code .. chars:sub(idx, idx)
73    end
74  
75    return code
76end
77
78-- 创建带干扰的验证码图像
79local function create_image(code)
80    local img = gd.createTrueColor(config.width, config.height)
81    local white = img:colorAllocate(255, 255, 255)
82    local black = img:colorAllocate(0, 0, 0)
83  
84    img:filledRectangle(0, 0, config.width, config.height, white)
85    
86    -- 绘制随机字符(带旋转和随机颜色)
87    for i = 1, #code do
88        local c = code:sub(i, i)
89        local color = img:colorAllocate(
90            math.random(30, 100),  -- R
91            math.random(30, 100),  -- G
92            math.random(30, 100)   -- B
93        )
94        -- 随机字体大小与旋转角度
95        local font_size = math.random(26, 32)
96        local x = 35 + (i - 1) * 30 + math.random(-8, 8)
97        local y = (config.height / 2 + 14) + math.random(-8, 8)
98    
99        img:stringFT(
100            color,
101            "/usr/share/fonts/xxx/xxx.ttf", 
102            font_size, 
103            0, 
104            x, 
105            y, 
106            c
107        )
108    end
109    
110    -- 添加噪点(100 个随机像素)
111    for _ = 1, 100 do
112        local color = img:colorAllocate(
113            math.random(0, 255),
114            math.random(0, 255),
115            math.random(0, 255)
116        )
117    
118        img:setPixel(
119            math.random(0, config.width), 
120            math.random(0, config.height), 
121            color
122        )
123    end
124    
125    return img
126end
127
128-- 主逻辑
129local red = connect_redis()
130    
131if not red then
132    return ngx.exit(500) -- 生成图片失败
133end
134
135-- 生成唯一标识与验证码
136local client_ip = get_client_ip()
137local user_agent = ngx.var.http_user_agent or "unknown"
138local redis_key = config.auth_key .. ":" .. client_ip .. "_" .. ngx.md5(user_agent)
139local white_key = config.whitelist_key .. ":" .. client_ip .. "_" .. ngx.md5(user_agent)
140
141local is_white, err = red:get(white_key)
142
143if is_white and is_white ~= ngx.null then -- 白名单禁止多次获取图像
144    return ngx.exit(403)
145end
146
147local code = generate_code()
148local img = create_image(code)
149
150-- 存储验证码到 Redis
151red:setex(redis_key, config.expire, code)
152close_redis()
153
154-- 输出图像响应
155ngx.header["Content-Type"] = "image/png"
156ngx.print(img:pngStrEx(6)) -- 返回图片数据
157

verify.html:

1<!DOCTYPE html>
2<html lang="zh">
3  <head>
4    <meta charset="UTF-8" />
5    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6    <title>系统校验/System check</title>
7
8    <style>
9      :root {
10        --color-text: #333;
11        --color-primary: #53aed6;
12        --color-info: #909399;
13        --color-background: #fff;
14        --radis-base: 4px;
15        --color-text-revert: #fff;
16        --color-danger: #f56c6c;
17      }
18
19      @media (prefers-color-scheme: dark) {
20        :root {
21          --color-text: rgba(200, 200, 200);
22          --color-background: #000;
23          --color-primary: #405761;
24          --color-text-revert: rgb(200, 200, 200);
25          --color-info: #909399;
26        }
27      }
28
29      * {
30        margin: 0;
31        padding: 0;
32        box-sizing: border-box;
33      }
34
35      body {
36        font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
37        font-size: 15px;
38        font-weight: 400;
39        color: var(--color-text);
40        color-scheme: light dark;
41        max-width: 100vw;
42        overflow-x: hidden;
43        background-color: var(--color-background);
44        transition: background 0.3s ease-out;
45        text-rendering: optimizelegibility;
46        -webkit-font-smoothing: antialiased;
47        -moz-osx-font-smoothing: grayscale;
48        line-height: 1;
49      }
50
51      button {
52        padding: 5px 10px;
53        outline: none;
54        border: none;
55        background-color: transparent;
56        cursor: pointer;
57        border-radius: var(--radis-base);
58        transition: opacity 0.3 ease-in;
59      }
60
61      .button:hover {
62        opacity: 0.8;
63      }
64
65      .button:active {
66        opacity: 0.7;
67      }
68
69      button.primary {
70        background-color: var(--color-primary);
71        color: var(--color-text-revert);
72      }
73
74      button.info {
75        background-color: var(--color-info);
76        color: #fff;
77      }
78
79      .main {
80        padding: 20px;
81        width: 100%;
82      }
83
84      .title {
85        font-size: 17px;
86        font-weight: 500;
87      }
88
89      .img-wrapper {
90        margin-top: 16px;
91      }
92
93      #captcha-img {
94        vertical-align: bottom;
95        border: 1px solid #ddd;
96        border-radius: var(--radis-base);
97      }
98
99      .input-wrapper {
100        margin-top: 12px;
101      }
102
103      #captcha-input {
104        padding: 0 5px;
105        height: 28px;
106        line-height: 28px;
107        outline: none;
108        border-radius: var(--radis-base);
109        border: 1px solid #ccc;
110        transition: border 0.3 ease-in;
111      }
112
113      #captcha-input:hover,
114      #captcha-input:focus {
115        border-color: var(--color-primary);
116      }
117
118      .result-wrapper {
119        padding: 8px;
120        display: none;
121        justify-content: center;
122        align-items: center;
123        margin-top: 12px;
124        width: 240px;
125        height: 80px;
126        border-radius: var(--radis-base);
127        background-color: var(--color-danger);
128        transition: background 0.3s ease-in;
129        color: #fff;
130        font-size: 14px;
131        word-break: break-all;
132      }
133    </style>
134  </head>
135  <body>
136    <div class="main">
137      <h1 class="title">请完成安全验证/Security verification required</h1>
138
139      <div class="img-wrapper">
140        <img id="captcha-img" width="200" height="120" />
141
142        <button class="button info" onclick="refreshCaptcha()">刷新/Refresh</button>
143      </div>
144
145      <div class="input-wrapper">
146        <input id="captcha-input" type="text" />
147
148        <button class="button primary" type="submit" onclick="verifyCaptcha()">
149          提交/Submit
150        </button>
151      </div>
152
153      <div class="result-wrapper"></div>
154    </div>
155
156    <script>
157      const img = document.getElementById("captcha-img");
158      const input = document.getElementById("captcha-input");
159      const resultEl = document.getElementsByClassName("result-wrapper")[0];
160
161      let verifying = false;
162
163      // 刷新验证码
164      function refreshCaptcha() {
165        // 添加随机参数防止缓存
166        img.src = "/captcha-image?" + Math.random().toString() + Date.now();
167      }
168
169      function onError(message) {
170        resultEl.style.display = "flex";
171        resultEl.textContent = message;
172      }
173
174      // 验证验证码
175      async function verifyCaptcha() {
176        if (verifying) {
177          return false;
178        }
179
180        const code = input.value;
181
182        if (!code || code.trim() === "") {
183          return onError("输入不能为空/Input cannot be empty");
184        }
185
186        verifying = true;
187
188        try {
189          const res = await fetch([`/captcha-verify?code=${code}`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.code.md));
190          const data = await res.json();
191
192          if (data.status === "success") {
193            window.location.href = "/";
194          } else {
195            onError(data.msg);
196            refreshCaptcha(); // 验证失败自动刷新
197            input.focus();
198          }
199        } catch (err) {
200          onError("验证失败, 请稍后重试/Verification failed, please try again later");
201        } finally {
202          verifying = false;
203        }
204      }
205
206      // 初始化验证码
207      window.onload = refreshCaptcha;
208      input.addEventListener("keydown", (e) => {
209        if (e.key.toLowerCase() === "enter") {
210          verifyCaptcha();
211        }
212      });
213    </script>
214  </body>
215</html>
216

效果显著,在删除 100 多个网段封禁后,在极短的时间内控制住了流量访问,外网出带宽使用率在初期出现了涨幅,十几分钟后趋于平稳。

--end


前端仔的一次运维实践》 是转载文章,点击查看原文


相关推荐


云开发Copilot实战:零代码打造智能体小程序指南
腾讯云开发CloudBase2025/10/22

云开发Copilot借助AI技术,实现小程序和Web应用的低代码生成与优化,大幅降低开发门槛,提升效率。无需编码,用户可通过自然语言描述需求,快速创建并发布应用,适合初创团队和快速迭代场景。 简述云开发的功能及优势 你是否曾经设想过,有一天只需简单描述需求,就能生成一个完整的小程序或 Web 应用,甚至连一行代码都无需编写?在如今快速发展的技术浪潮中,低代码开发正在重新定义开发效率,而腾讯云的云开发 Copilot 正是其中的佼佼者。借助 AI 技术,它不仅能够迅速生成应用和页面,还能优化样式、


用 Python 揭秘 IP 地址背后的地理位置和信息
烛阴2025/10/21

准备工作:安装必备工具 首先,请确保你的Python环境中安装了requests库。 pip install requests 第一步:查询自己的公网 IP 信息 import requests import json # 向ipinfo.io发送请求,不带任何IP地址,它会默认查询你自己的IP url = "https://ipinfo.io/json" try: response = requests.get(url) response.raise_for_status


使用AI编程工具的“经济学”:成本控制与性能优化策略
rengang662025/10/20

最近跟几个朋友聊天,发现大家都在用AI编程工具,比如Cursor、Claude Code、Codex等。聊到兴头上,我问了一个“煞风景”的问题:“兄弟们,这月API账单看了吗?” 空气突然安静。 没错,AI编程工具确实香,写代码、改Bug、写文档,效率起飞。但“免费的午餐”总是短暂的,当我们真正把它用在项目里,或者用量一大起来,那账单就跟坐了火箭一样往上蹿。今天,我就想跟大家聊聊,作为一个“精打细算”的程序员,我们怎么在享受AI便利的同时,把成本控制得死死的,实现“降本增效”的终极目标。 这篇文


LeetCode 402 - 移掉 K 位数字
网罗开发2025/10/19

文章目录 摘要描述题解答案题解代码分析代码逻辑逐步拆解: 示例测试及结果时间复杂度空间复杂度总结 摘要 在很多前端或后端的业务逻辑中,我们经常要处理数字的“裁剪”问题,比如在账单明细里自动保留最小金额组合、或在数据压缩时尽量保留较小值。LeetCode 第 402 题《移掉 K 位数字》(Remove K Digits)就是一个非常贴近这种逻辑的算法题。 题目的核心是:给定一个非负整数(以字符串形式表示),从中移除 k 个数字,使得剩下的数字最小化。 看似简单,但


谷歌 × 耶鲁联手发布抗癌神器!AI 推理精准狙击「隐身」癌细胞
新智元2025/10/17

「【新智元导读】近日,谷歌与耶鲁大学联合发布的大模型 C2S-Scale,首次提出并验证了一项全新的「抗癌假设」。这一成果表明,大模型不仅能复现已知科学规律,还具备生成可验新科学假设的能力。」 刚刚,AI 科学应用领域又有一件大事发生! 谷歌与耶鲁大学的科学家们联合发布了一个大模型 Cell2Sentence-Scale 27B(C2S-Scale)。 该模型提出了一个关于癌细胞行为的全新假设,并在多次体外实验中得到验证。 这一发现引发广泛关注,它展示了人工智能模型生成原创科学假设的潜力,有望


【ComfyUI】Animate单人物角色视频替换
Mr数据杨2025/10/16

在智能创作和视频生成的实践中,工作流不仅仅是节点的堆叠,而是一个围绕业务目标、数据流转和模型能力的有机整体。通过对工作流的抽象与模块化设计,我们可以在复杂的生成任务中实现高效的可复用性与灵活性。本篇文章将围绕某实际业务场景展开,介绍完整的工作流结构,并重点解析核心模型与节点配置方式,帮助读者从整体视角理解其逻辑与实现。 文章目录 工作流核心模型工作 Node 节点工作流程应用场景 开发与应用 工作流 一个完整的工作流是由数据输入、模型处理、结果合成和输出展示构成的有机链路。


如何在Linux服务器上部署jenkins?
Broken Arrows2025/10/15

一,首先安装JAVA环境 Java8、java11、java17(JRE或者 JDK都可以),从 Jenkins2.357(于2022年6月28日发布)和2.361.1LTS版本开始,Jenkins需要Java11或更高版本。此外,从Jenkins2.355(2022年6月14日发布和Jenkins2.346,1LTS(2022年6月22日发布)开始,Jenkins支持Java 17。 一般我们推荐使用包管理器来进行安装,我这里使用的是Ubuntu的系统,在这里给以下步骤做参考。 # 更新


将 GPU 级性能带到企业级 Java:CUDA 集成实用指南
程序猿DD2025/10/13

引言 在企业软件世界中,Java 依靠其可靠性、可移植性与丰富生态持续占据主导地位。 然而,一旦涉及高性能计算(HPC)或数据密集型作业,Java 的托管运行时与垃圾回收开销会在满足现代应用的低延迟与高吞吐需求上带来挑战,尤其是那些涉及实时分析、海量日志管道或深度计算的场景。 与此同时,最初为图像渲染设计的图形处理器(GPU)已成为并行计算的实用加速器。 像 CUDA 这样的技术让开发者能够驾驭 GPU 的全部算力,在计算密集型任务上获得显著的加速效果。 但问题在于:CUDA 主要面向 C/C+


服务端之NestJS接口响应message编写规范详解、写给前后端都舒服的接口、API提示信息标准化
焊码IoT2025/10/12

MENU 前言定义提示信息设计原则提示信息风格分类提示信息模板化设计国际化与多语言支持最佳实践参考示例(NestJS响应)总结统一风格示例清单推荐API响应message清单(可直接使用) 前言 在现代后端开发中,接口响应不仅仅是数据的传递,还承担着向前端或用户传递操作状态和结果的功能。一个规范、统一的message字段设计,可以显著提升系统的可维护性、前端开发效率和用户体验。 定义 响应结构示例(NestJS风格) return { statu


Ling-1T:蚂蚁百灵如何以“非思考”策略,开启万亿参数效率新篇章?
墨风如雪2025/10/10

2025年10月9日,AI世界再次被一颗“重磅炸弹”点燃。蚂蚁集团百灵大模型团队正式发布了其Ling 2.0系列的首款旗舰模型——Ling-1T。这不仅仅是一个拥有万亿参数的通用大语言模型,它更代表着蚂蚁集团在大模型设计理念上的一次大胆创新和实践突破。它已全面开源,正等待着全球开发者共同探索其无限潜力。 “非思考”定位:速度与精准的完美结合 初听“非思考模型”,你或许会感到好奇。这并非 Ling-1T 不具备推理能力,而是蚂蚁集团对大模型家族的一种策略性划分。在百灵模型矩阵中,“Ling系列”

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0