nginx+lua进行代理访问控制

nginx+lua进行代理访问控制

nginx+lua进行代理访问控制

原理:
简单说就是需要先访问一个认证接口,通过后才对此IP的后续请求放行。

备注:
早期很长时间我这个云服务器就使用的这个策略,不过后来改成了 UDP 敲门了,原理与这个差不多。

流程

  1. 悄悄提供一个隐蔽的 http 接口,需要先到这个 URL 进行鉴权。
  2. 通过鉴权后,将IP地址放入缓存和redis。
  3. 对其它接口(http+tcp)的访问,每次都去缓存内查询IP是否可信。

这里使用的是 nginx 的衍生版本 openresty

一个简单的认证逻辑

1
 curl -i -d "name=<占位内容>&key=$(($(date +%s)*9-3000))" https://xxxx.xxxx.cn/sakdwe/wefewoiefwe

值为(当前时间戳*N-3000),服务器那边需要还原出来,如果等于服务器当前的时间戳(误差5s内),就通过;
当然也可以做成其它更复杂适用的认证逻辑;

本实例中只有先通过了这个 http 的认证,才能访问代理的 http资源和tcp资源。

说明

  1. 当前设定只同时支持1个IP,当然可以改为 hash 类型, 保留多个可信任IP
  2. TCP资源代理当然也支持 SSH。

nginx 配置文件 nginx.conf

针对 http 的设置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
http {
    lua_shared_dict http_acl_zone 12k;      # 设置一个共享内存

    server {
        listen       80;

        # 秘密的认证接口和lua脚本
        location = /sakdwe/wefewoiefwe {
            content_by_lua_file /opt/app/openresty/nginx/conf/set_acl.lua;
        }

        location / {
            # 每次访问都先到这一步进行鉴权
            access_by_lua_file /opt/app/openresty/nginx/conf/http_acl.lua;
            root   html;
            index  index.html index.htm;
        }
    }
}

针对 TCP 端口的设置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
stream {
    lua_shared_dict tcp_acl_zone 12k;

    # 友好的日志格式
    log_format proxy '$remote_addr [$time_local] '
                    '$protocol $status $bytes_sent $bytes_received $session_time "$upstream_addr" '
                    '"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time"';

    server {
        access_log  logs/tcp-ssh.log  proxy;
        listen 52222;       # 需要代理某个 TCP 端口
        preread_by_lua_file /opt/app/openresty/nginx/conf/tcp_acl.lua;  # 鉴权脚本
        proxy_connect_timeout 5s;
        proxy_timeout 120s;
        proxy_pass 127.0.0.1:22;
    }
}

认证逻辑 set_acl.lua

– 描述:
– 先校验: 是post请求,post参数内需要有一个 key,按设定的逻辑还原出来验证
– 通过后将真实IP先写入内部共享缓存,再写入 redis 缓存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
local function set_http_acl_zone()
    local ip = ngx.var.remote_addr    -- 获取用户真实IP地址

    -- 先设置nginx内部共享变量
    local http_acl_zone = ngx.shared.http_acl_zone
    http_acl_zone:set("ops_ip",ip)

    -- 再设置 redis key 值
    local redis = require "resty.redis"
    local red = redis:new()
    red:set_timeout(1000)       -- 连接超时时间设定 1 sec

    local ok, err = red:connect("127.0.0.1", 26379)
    if not ok then
        return false
    end

    # 将用户IP写入 redis
    ok, err = red:set("ops_ip", ip)
    if not ok then
        return false
    else
        ngx.log(ngx.ERR, "acl ip write ops_ip, ")
        return true
    end
end

local function verify_ip()
    -- 校验请求参数是否合规
    if ngx.var.request_method ~= "POST" then
        ngx.exit(ngx.HTTP_FORBIDDEN)
    end

    ngx.req.read_body()
    local post_args = ngx.req.get_post_args()
    -- local name = post_args["name"]
    local pw = post_args["key"]

    if not pw then
        return false
    end

    local rtime = os.time() - (pw+3000)/9

    if math.abs(rtime) < 5 then
        return true
    else
        return false
    end
end

if(verify_ip() == true) then
    set_http_acl_zone()
    ngx.exit(ngx.HTTP_NOT_ALLOWED)  -- 405, 校验成功
    return
else
    ngx.exit(ngx.HTTP_NOT_FOUND)    -- 404
end

HTTP接口鉴权逻辑 http_acl.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- 校验当前IP是否与nginx共享内存中的IP地址一致
local function ops_acl()
    local clientip = ngx.var.remote_addr

    local http_acl_zone = ngx.shared.http_acl_zone
    local acl_ip = http_acl_zone:get("ops_ip")

    if (acl_ip ~= nil) then
        -- 共享变量内有数据
        if (clientip==acl_ip) then
            return true
        else
            return false
        end
    else
        return false
    end
end

if(ops_acl() ~= true) then
    ngx.log(ngx.ERR, "request ip not in ops_ip")
    ngx.exit(ngx.HTTP_FORBIDDEN)
    return
else
    return
end

TCP代理端口鉴权逻辑 tcp_acl.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
-- 用于 TCP 代理验证用户IP是否在 共享内存 或 redis 中,如果不在则拒绝代理

local function ops_acl()
    local clientip = ngx.var.remote_addr

    local tcp_acl_zone = ngx.shared.tcp_acl_zone
    local acl_ip = tcp_acl_zone:get("ops_ip")

    -- 共享变量内有数据
    if (acl_ip ~= nil) then
        if (clientip==acl_ip) then
            return true
        end
    end

    -- 共享变量内无数据,或与限制IP不一致(因为http和tcp的变量不同步)
    -- 则从redis中读一次

    ngx.log(ngx.ERR, "acl link redis +1")
    local redis = require "resty.redis"
    local red = redis:new()
    red:set_timeout(1000)       -- 1 sec

    local ok, err = red:connect("127.0.0.1", 26379)
    if not ok then
        return false
    end

    local res, err = red:get("ops_ip")
    if not res then
        return false            -- redis 中无数据
    end

    if res == ngx.null then
        return false            -- redis 中无数据
    end

    if (clientip==res) then
        -- 同时也设置一份到内存变量,避免以后再读redis
        local tcp_acl_zone = ngx.shared.tcp_acl_zone
        tcp_acl_zone:set("ops_ip",clientip)

        return true
    else
        return false
    end

end

if(ops_acl() ~= true) then
    ngx.log(ngx.ERR, "tcp request ip != ops_ip, ")
    ngx.exit(403)
    return
else
    return
end

微信搜索IT运维小秋

Licensed under CC BY-NC-SA 4.0
转载或引用本文时请遵守许可协议,知会作者并注明出处
不得用于商业用途!
最后更新于 2021-09-10 00:00 UTC