利用openresty nginx的lua脚本为poste.io邮箱添加访问验证码

recallmc 发布于 2026-02-02 304 次阅读


关于openresty的安装可以查看Linux Docker 部署openresty nginx – recallmc blog

关于poste.io可以查看Linux docker 部署poste.io+nginx – recallmc blog

1、准备相关lua脚本

captcha_api.lua

-- 验证码生成API - 单点生成
-- 生成验证码并返回JSON数据

local chars = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
local code = ""
for i = 1, 6 do
    local rand = math.random(#chars)
    code = code .. string.sub(chars, rand, rand)
end

-- 生成唯一key
local timestamp = ngx.now()
local key = "captcha_" .. ngx.md5(code .. timestamp .. math.random(1000, 9999))

-- 存储到共享内存(确保存储)
local store = ngx.shared.captcha_store
local success, err = store:set(key, code, 300)  -- 5分钟过期

if not success then
    ngx.log(ngx.ERR, "验证码存储失败: ", err)
    ngx.status = 500
    ngx.say('{"success":false,"message":"生成验证码失败"}')
    return
end

-- 生成SVG图片
local width = 200
local height = 80

local svg = string.format([[


]], width, height, width, height)

-- 干扰线
for i = 1, 5 do
    svg = svg .. string.format([[

]], math.random(width), math.random(height), math.random(width), math.random(height))
end

-- 验证码文字
for i = 1, #code do
    local char = string.sub(code, i, i)
    local x = 30 + (i - 1) * 28
    local y = 50
    local rotate = math.random(-15, 15)
    
    svg = svg .. string.format([[

    %s
]], x, y, rotate, x, y, char)
end

svg = svg .. "\n"

-- 返回JSON(包含所有数据)
local response = string.format([[
{
    "success": true,
    "data": {
        "key": "%s",
        "code": "%s",
        "svg": "%s"
    },
    "timestamp": %f
}
]], key, code, ngx.escape_uri(svg), timestamp)

ngx.header["Content-Type"] = "application/json; charset=utf-8"
ngx.print(response)

-- 记录日志(调试用)
ngx.log(ngx.INFO, "生成验证码: key=", key, ", code=", code)

login_page.lua,这段代码有部分html代码会被识别直接显示页面,所以我放在下方文件里了,自行点击下载,然后复制到你创建的login_page.lua脚本里就行

verify_api.lua

-- 验证码验证API

local args = ngx.req.get_uri_args()
local key = args.key
local code = args.code

if not key or not code then
    ngx.header["Content-Type"] = "application/json"
    ngx.say('{"success": false, "message": "缺少参数"}')
    return
end

-- 从共享内存获取验证码
local store = ngx.shared.captcha_store
local stored_code = store:get(key)

-- 记录日志
ngx.log(ngx.INFO, "验证请求: key=", key, 
       " 输入=", code, 
       " 存储=", stored_code or "nil",
       " IP=", ngx.var.remote_addr)

if not stored_code then
    ngx.header["Content-Type"] = "application/json"
    ngx.say('{"success": false, "message": "验证码已过期"}')
    return
end

-- 验证验证码(不区分大小写)
if string.upper(stored_code) ~= string.upper(code) then
    store:delete(key)
    ngx.header["Content-Type"] = "application/json"
    ngx.say('{"success": false, "message": "验证码错误"}')
    return
end

-- 验证成功,删除验证码
store:delete(key)

-- 创建会话
local session_id = "sess_" .. ngx.md5(key .. ngx.now() .. math.random(1000, 9999))
store:set("session_" .. session_id, "valid", 3600)

ngx.header["Content-Type"] = "application/json"
ngx.say('{"success": true, "session": "', session_id, '", "message": "验证成功"}')

将准备好的lua脚本放入指定的lua脚本目录,例如我指定的目录是/data/openresty/lua

2、修改nginx的配置

首先进入我们部署好的docker容器中

docker exec -it openresty bash

修改nginx的主要配置文件

cd /usr/local/openresty/nginx/conf
vim nginx.conf

将如下代码复制到http块中

# 必须的 lua 包路径配置
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
lua_shared_dict captcha_store 10m;

按住shift输入英文冒号,输入wq保存并退出,输入exit退出容器

将如下代码替换之前的nginx反向代理配置,注意域名替换成自己的

server {
    listen 80;
    listen [::]:80;
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    # 原有 server_name,可继续新增更多当前证书支持的域名
    server_name mail.recallmc.com;
    # ========== 核心API ==========
        
        # 1. 生成验证码(同时返回图片和数据)
        location = /api/captcha {
            default_type application/json;
            add_header Cache-Control "no-cache, no-store, must-revalidate";
            add_header Pragma "no-cache";
            add_header Expires "0";
            
            content_by_lua_file /etc/nginx/lua/captcha_api.lua;
        }
        
        # 2. 验证验证码
        location = /api/verify {
            default_type application/json;
            
            content_by_lua_file /etc/nginx/lua/verify_api.lua;
        }
        
        # ========== 页面 ==========
        
        # 3. 登录页面(不生成验证码,只显示页面)
        location = /login {
            content_by_lua_file /etc/nginx/lua/login_page.lua;
        }
        
        # ========== 访问控制 ==========
        
        # 4. 主访问控制
        location / {
            
            # 白名单
            if ($uri = /login) {
                break;
            }
            
            if ($uri = /api/captcha) {
                break;
            }
            
            if ($uri = /api/verify) {
                break;
            }
            
            # 检查认证
            if ($cookie_captcha_session != "verified") {
                return 302 /login;
            }
            # 其它配置
            client_max_body_size 75M;  # 允许的最大文件大小为 20MB
            proxy_pass https://poste-mail;
            proxy_set_header Host $host;
            # real-ip
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header REMOTE-HOST $remote_addr;
            # websocket
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_read_timeout 86400;
            ## replace content ##
            sub_filter_once  off;
            sub_filter '撰写新邮件' '写信';
            sub_filter 'Dark mode' '深色';
            sub_filter 'Light mode' '浅色';
            sub_filter '[Administration]' '控制台';
            sub_filter '>Administration<' '>控制台<';
            sub_filter 'Trusted Senders' '可信发件人';
            sub_filter 'Collected Recipients' '收件人集合';
            sub_filter '' '\n.pro,.brand,.nav-sidebar p.alert{display:none !important}\n';
        }
        
        # ========== 错误页面 ==========
        
        error_page 401 /login;
        error_page 403 /login;
        error_page 404 /404.html;
        error_page 500 502 503 504 /50x.html;
        
        location = /404.html {
            internal;
            return 404 '{"error": "Not Found"}';
        }
        
        location = /50x.html {
            internal;
            return 500 '{"error": "Internal Server Error"}';
        }
}

3、重载配置

docker exec openresty nginx -s reload