nginx+lua实现人机身份验证

发布时间 2023-10-09 16:10:40作者: suruozhong

前言

现在很多网站考虑安全,会做人机验证,可以有效的防刷,防爬虫,防止暴力破解。
你是否遇到过这个
0
这个
0
还有这个
0

如何实现?

如何实现人机验证,又不用和前端耦合在一起,类似waf,在第一层做验证,不通过就直接拦截。nginx支持调用lua脚本,于是可以使用nginx_lua_module来实现。
我们先看看Nginx指令处理阶段:
0
访问某个需要人机验证的页面时,在content_by_lua做处理,如果没经过人机验证,我们返回人机验证页面,浏览器人机验证后,做缓存和cookies,我定了12个小时。也就是12小时内任何页面不需要再做人机验证。
大概的流程如下:
 
0

实现人机验证步骤

  1. 申请谷歌recaptcha接口参数,https://www.google.com/recaptcha/admin
  2. 安装lua环境,nginx编译安装lua-nginx-module
  3. nginx配置需要做人机验证的页面
    # 网站需要人机验证的页面
    location ~ ^(/index|/public|/search|search/(\d*))$ { 
        content_by_lua_file lua/recaptcha.lua;
    }
  1. nginx配置需要过滤的restfulApi

 

# 人机验证通过,需要过滤的接口
location ~ ^(/api1|api2|api3)$ { 
    include vhost/valid.conf;
    access_by_lua_file lua/api_filter.lua;
}

 

 

 

  1. 编写recaptcha.lua做页面人机验证逻辑
-- 1、解密cookies,ip是否已存在缓存
-- local headers = ngx.req.get_headers()
-- 如果已存在验证过的cookies  cap,解析正确则不拦截
local zhong_cap = ngx.var.cookie_cap
if zhong_cap and #zhong_cap > 0 then
    local aes_128_cbc_with_iv = assert(aes:new(key,nil, aes.cipher(128,"cbc"), {iv=iv}))
    -- AES 128 CBC with IV and no SALT
    local cipherBytes = ngx.decode_base64(zhong_cap)
    local cache_ip = aes_128_cbc_with_iv:decrypt(cipherBytes)
    if cache_ip ~= nil and cache_ip and #cache_ip > 0 then
        ngx.log(ngx.INFO, "-------------cache_ip-------------"..cache_ip)
        local request_ip = ngx.var.ip
        ngx.log(ngx.INFO, "-------------request_ip-------------"..request_ip)
        if cache_ip == request_ip then
            cap_cache = "1"
            -- redis续期
            redis_util.set_redis("cap:"..request_ip,cap,cap_second)
            -- ip已经缓存过,直接放行
            ngx.log(ngx.INFO, "-------------正常转发-------------")
            ngx.exec("@proxy_pass")
            return;
        end
    end
end

ngx.log(ngx.INFO, "-------------人机验证-------------"..json.encode(ngx.var.request_uri))
local html = 
"<script>"
.."    window.onload = function() {"
.."        window.challenge_conf = {'is_interactive':1,'cserver_addr':'"/recaptcha/html/cap3.html','rule_id':1 };"
.."        var challenge_uri = '"/recaptcha/html/cap3.html';"
.."        var xhr = new XMLHttpRequest();"
.."        xhr.open('GET', challenge_uri + '?&' + Math.random());"
.."        xhr.send();"
.."        xhr.onload = function() {"
.."          if (xhr.status == 200) {"
.."         console.log(xhr.response);"
.."            document.write(xhr.response);"
.."            document.close();"
.."          }"
.."        };"
.."    }"
.."</script>"
.."<p>加载中...</p>"
ngx.header.content_type="text/html;charset=utf8"
ngx.say(html) 
return

 

  1. 编写api_filter过滤是否通过人机验证逻辑
-- 判断白名单(白名单直接通过)
for key, value in ipairs(white_apis) do
    local url = value["url"]
    local args = value["args"]
    if(url == request_uri) then
        ngx.log(ngx.INFO,"3")
        if not args then
            -- 白名单,无参数,直接通过
            ngx.exec("@proxy_pass")
            return
        else
            local flag = 1
            for args_k,args_v in pairs(args) do
                local request_v = tostring(arg_json[args_k])
                args_v = tostring(args_v)
                if(args_v=="" or args_v == nil or args_v == 'nil')then
                    if(request_v == "" or request_v == nil or request_v == 'nil')then
                    else
                        flag = 0
                    end
                else
                    if(args_v == request_v) then
                    else
                        flag = 0
                    end
                end
            end

            if(flag == 1)then
                ngx.log(ngx.INFO,"白名单通过"..request_url)
                ngx.exec("@proxy_pass")
                return;
            end

        end
    end
end

-- 判断接口是否已通过人机验证
local remoteIp = ngx.var.ip
local val = redis_util.get_redis("cap:"..remoteIp)
ngx.log(ngx.INFO,"缓存"..json.encode(val))

if(val == "null")then
    ngx.exit(403)
    -- ngx.header.content_type="application/json;charset=UTF-8"
    -- local ret = {
    --     code= "200",
    --     data= {
    --         current= 1,
    --         size=15,
    --     },
    --     msg= "操作成功",
    --     success= true,
    -- }
    -- ngx.say(json.encode(ret)) 
    return;
else
    ngx.exec("@proxy_pass")
    return;
end
  1. 实现效果如下:
0
验证成功后,会自动跳到具体页面,不成功显示失败页面。
我用的是用户端无交互的人机验证。也可以对接需要交互的,就是点击验证码,或者点击什么都可以。可自行自定义
 

最后

具体源码不免费,有需要可以联系QQ839293390,可定制化实现、部署。