nginx+lua进行代理访问控制
原理:
简单说就是需要先访问一个认证接口,通过后才对此IP的后续请求放行。
备注:
早期很长时间我这个云服务器就使用的这个策略,不过后来改成了 UDP 敲门了,原理与这个差不多。
流程
- 悄悄提供一个隐蔽的 http 接口,需要先到这个 URL 进行鉴权。
- 通过鉴权后,将IP地址放入缓存和redis。
- 对其它接口(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个IP,当然可以改为 hash 类型, 保留多个可信任IP
- 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
|