首页 > *nix技术, nginx > 利用openresty代理实现自动认证和受限资源获取

利用openresty代理实现自动认证和受限资源获取

2022年4月3日 发表评论 阅读评论 1,940 次浏览

一,环境与源码包

$ cat /etc/issue
Ubuntu 20.04.2 LTS \n \l
$ uname -a
Linux lenky-HP 5.10.0-1057-oem #61-Ubuntu SMP Thu Jan 13 15:06:11 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
$ ls
nginx-1.20.2.tar.gz  openresty-1.19.9.1.tar.gz

二,目标模型
client –> openresty –> nginx1/nginx2
说明:
1,nginx被用来作为后端服务,也就是会启动两个nginx+php,模拟web1站点和web2站点。
2,openresty用来作为请求分发服务器,将客户端不同的请求分发到web1或web2。
3,web2站点的资源访问需要做用户名/密码登录验证,但这个验证要对client透明,也就是client并不会去输用户名/密码。所以,这个验证过程需要由openresty来做,在openresty的本地保存有web2站点的账号/密码。

二,搭建nginx+php的站点
省略

三,搭建openresty
1,安装依赖包
$ sudo apt-get install openssl libssl-dev libpcre3 libpcre3-dev zlib1g-dev

2,编译安装
$ ./configure
$ make
$ sudo make install
默认安装在:$ ls -l /usr/local/openresty

3,修改nginx.conf,添加对lua的支持

$ sudo vim /usr/local/openresty/nginx/conf/nginx.conf
$ sudo cat /usr/local/openresty/nginx/conf/nginx.conf |grep lua_package -B 1 -A 1
http {
    lua_package_path "/usr/local/openresty/lualib/?.lua;;";
    lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
    include /usr/local/openresty/nginx/lua/lua.conf;

4,创建lua配置和lua脚本

$ cd /usr/local/openresty/nginx
$ sudo mkdir lua
$ sudo chmod 777 lua
$ cd lua/
$ vi lua.conf
$ cat lua.conf 
server {
    listen       8080;   
    server_name  _;    
 
    location /lua_hello {
        default_type 'text/html';    
        content_by_lua_file /usr/local/openresty/nginx/lua/hello.lua;  
    }
}
$ vi hello.lua
$ cat hello.lua
ngx.say("hello world");

5,测试配置

$ sudo /usr/local/openresty/nginx/sbin/nginx -t
nginx: the configuration file /usr/local/openresty/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/openresty/nginx/conf/nginx.conf test is successful

6,启动openresty和测试业务效果

$ sudo /usr/local/openresty/nginx/sbin/nginx
$ ps aux | grep nginx
root      317226  0.0  0.0  11796  1324 ?        Ss   11:24   0:00 nginx: master process /usr/local/openresty/nginx/sbin/nginx
nobody    317227  0.0  0.0  12456  4008 ?        S    11:24   0:00 nginx: worker process
$ wget -q -O -  127.0.0.1:8080/lua_hello
hello world

四,配置openresty的代理
web2下有个php文件,客户端直接访问web2是受限的:

$ wget --no-check-certificate -q -O - https://192.168.122.237/web2/system/user_create_page.php
<script language="javascript">
                window.location = "denied.html"
            </script>

要到登录https://192.168.122.237/web2/login.php先进行登录,才能访问。
现在要实现客户端访问openresty,然后由openresty去自动登录web2并且将对应的资源请求下来发给客户端。

0,直接访问openresty的web2是不通

$ wget -O - http://127.0.0.1:8080/web2/login.php
--2022-04-03 14:27:41-- http://127.0.0.1:8080/web2/login.php
正在连接 127.0.0.1:8080... 已连接。
已发出 HTTP 请求,正在等待回应... 404 Not Found
2022-04-03 14:27:41 错误 404:Not Found。

1,给openresty加上web2的upstream配置
在nginx.conf的http里添加如下:
upstream web2 {
server 192.168.122.237:443;
}
然后在lua.conf的server里添加如下:
location /web2 {
proxy_pass https://web2;
}

2,重新加载配置,测试

$ sudo /usr/local/openresty/nginx/sbin/nginx -s reload
$ wget -O - http://127.0.0.1:8080/web2/login.php
</html>
-                                     100%[======================================================================>]   4.80K  --.-KB/s    用时 0s    

2022-04-03 14:32:15 (324 MB/s) - 已写入至标准输出 [4914/4914]

也就是将https://192.168.122.237:443/web2/login.php通过openresty返回到客户端了。

3,访问受限资源

$ wget -q -O - http://127.0.0.1:8080/web2/system/user_create_page.php
<script language="javascript">
                window.location = "denied.html"
            </script>

同样是受限。
接下来要实现上面这个请求能够正常访问,即由openresty去web2自动登录认证,然后将user_create_page.php资源请求下来发给客户端。

五,用lua脚本实现的openresty自动代理认证
1,先修改lua.conf,对location进行一下拆分,把认证接口单独为一个location,然后把其他请求丢到lua里去处理,完整的代码如下:

server {
    listen       8080;
    server_name  _;

    location /lua_hello {
        default_type 'text/html';
        content_by_lua_file /usr/local/openresty/nginx/lua/hello.lua;
    }

    location /web2/rest/auth.php {
        proxy_pass https://web2;
    }

    location /web2 {
        content_by_lua_file /usr/local/openresty/nginx/lua/web2.lua;
    }

    location /web2_internal/ {
        internal;
        proxy_pass https://web2/;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        #proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

认证接口/web2/rest/auth.php单独出来,直接proxy_pass到web2。
其他web2请求(仍然假设到web2的请求都是在/web2下)交给lua/web2.lua处理。
添加一个/web2_internal/的内部代理,末尾要添加/,方便能转到web2站点的正确路径。可以在web2上查看access_log进行确认。

2,重点是lua/web2.lua文件,内容如下:


local cjson = require("cjson") 

local res = ngx.location.capture("/web2/rest/auth.php", {
    method = ngx.HTTP_POST,
    args = {},
    body = '{"username": "admin","password":"a1bc6ce70630870bf20166791197c57eb174eb4e16e36693612b3e499cc115b4","login": true}'
})

--ngx.say(res.status)
--ngx.say(res.body)
--[[
200
{"success":true,"result":{"token":"co4wgw8gsgoc","url":"system\/dashboard_page.php"}}
]]

local body_json = cjson.decode(res.body)
--ngx.say(body_json.success)
if body_json.success ~= true then
    ngx.exit(403)
end

--[[
ngx.say("res.header:")
for k,v in pairs(res.header) do
    ngx.say(tostring(k)..":"..tostring(v))
end
]]
--[[
X-Powered-By:PHP/5.5.15
Set-Cookie:PHPSESSID=72p27dfkgcob0q2ftveo4ggjo6; path=/; secure; HttpOnly
Expires:Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control:no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma:no-cache
X-PHP-Response-Code:200
Content-Type:application/json
Strict-Transport-Security:max-age=31536000; includeSubDomains
Content-Length:85
]]

local token = ""
for k,v in pairs(res.header) do
    local k_str = tostring(k)
    local v_str = tostring(v)
    if k_str == "Set-Cookie" and string.find(v_str, "PHPSESSID=") ~= nil then
        local i = string.find(v_str, "PHPSESSID=")
        local j = string.find(v_str, ";", i)
        if j == nil then
            j = -1
        end
        token = string.sub(v_str, i, j)
    end
end

--ngx.say(token)
if token == "" then
    ngx.exit(403)
end

local req_cookie = ngx.req.get_headers()["cookie"]
if req_cookie == nil then
    ngx.req.set_header("cookie", token)
elseif string.find(req_cookie, "PHPSESSID=") == nil then
    ngx.req.set_header("cookie", token..req_cookie)
else
-- do not modify the old sessionid
end

--ngx.say(ngx.var.request_uri);
ngx.exec("/web2_internal"..ngx.var.request_uri)

这个逻辑要根据web2的认证接口的请求数据和响应数据进行反复的调。整个的基本逻辑就是先用子请求ngx.location.capture(“/web2/rest/auth.php”来进行自动认证,认证通过后,将对应的session id(也就是cookie)添加到主请求的请求头里,再用ngx.exec跳转location即可。另外,账号/密码可以从redis或文件里读取,这里测试就是直接写在代码里。

六,进一步优化
客户端的每个请求都发起认证子请求,性能上是不合适的。因此,可以把cookie写到响应头里发给客户端,后续的请求先判断是否已存在这个token,如果存在就可以不用发认证子请求了。

在开始进一步修改之前,客户端获得的响应头如下:
—response begin—
HTTP/1.1 200 OK
Server: openresty/1.19.9.1
Date: Sun, 03 Apr 2022 04:05:08 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Powered-By: PHP/5.5.15
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache

可以看到没有cookie的设置,改一下web2.lua,把认证token写到客户端。修改代码如下:

local token = ""
local set_cookie = ""
for k,v in pairs(res.header) do
    local k_str = tostring(k)
    local v_str = tostring(v)
    if k_str == "Set-Cookie" and string.find(v_str, "PHPSESSID=") ~= nil then
        local i = string.find(v_str, "PHPSESSID=")
        local j = string.find(v_str, ";", i)
        if j == nil then
            j = -1
        end
        token = string.sub(v_str, i, j)
        set_cookie = v_str
    end
end

ngx.header["Set-Cookie"] = set_cookie

添加了三行set_cookie的代码,在客户端再请求,就能看到Set-Cookie的响应头了。

---response begin---
HTTP/1.1 200 OK
Server: openresty/1.19.9.1
Date: Sun, 03 Apr 2022 04:14:00 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
Set-Cookie: PHPSESSID=fpf287b6n3mkmguol7d29pgfn6; path=/; secure; HttpOnly
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Powered-By: PHP/5.5.15
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache

跑通基本逻辑,其他的根据需求再改改,重构的完整代码如下:


-- file: web2.lua

local function auth()
    res = ngx.location.capture("/web2/rest/auth.php", {
        method = ngx.HTTP_POST,
        args = {},
        body = '{"username": "admin","password":"a1bc6ce70630870bf20166791197c57eb174eb4e16e36693612b3e499cc115b4","login": true}'
    })

    if res == nil then
        ngx.exit(401)
    end

    --ngx.log(ngx.ERR, res.status)
    --ngx.log(ngx.ERR, res.body)
    --for k,v in pairs(res.header) do
    --    ngx.log(ngx.ERR, tostring(k)..":"..tostring(v))
    --end

    return res;
end

local function get_cookie_from_auth(header)
    cookie_session_id = ""
    cookie_value = ""

    for k,v in pairs(header) do
        local k_str = tostring(k)
        local v_str = tostring(v)
        if k_str == "Set-Cookie" and string.find(v_str, "PHPSESSID=") ~= nil then
            local i = string.find(v_str, "PHPSESSID=")
            local j = string.find(v_str, ";", i)
            if j == nil then
                j = -1
            end
            cookie_session_id = string.sub(v_str, i, j)
	        cookie_value = v_str
            break
        end
    end

    --ngx.log(ngx.ERR, cookie_session_id)
    --ngx.log(ngx.ERR, cookie_value)
    return cookie_session_id, cookie_value
end

local function auth_is_pass()
    local req_cookie = ngx.req.get_headers()["cookie"]
    --ngx.log(ngx.ERR, req_cookie)

    if req_cookie == nil then
        return false
    elseif string.find(req_cookie, "PHPSESSID=") == nil then
        return false
    else
        return true
    end
end


local function add_session_id_to_req(cookie_session_id)
    local req_cookie = ngx.req.get_headers()["cookie"]
    --ngx.log(ngx.ERR, req_cookie)

    if req_cookie == nil then
        ngx.req.set_header("cookie", cookie_session_id)
    elseif string.find(req_cookie, "PHPSESSID=") == nil then
        ngx.req.set_header("cookie", cookie_session_id..req_cookie)
    else
        -- do not modify the old sessionid
    end
end

-- main
if auth_is_pass() == false then
    local res = auth()
    local cjson = require("cjson")
    local body_json = cjson.decode(res.body)

    --ngx.log(ngx.ERR, body_json.success)
    if body_json.success ~= true then
        ngx.exit(402)
    end

    cookie_session_id, cookie_value = get_cookie_from_auth(res.header)
    if cookie_session_id == "" then
        ngx.exit(403)
    else
        add_session_id_to_req(cookie_session_id)
    end
    ngx.header["Set-Cookie"] = cookie_value
end

-- redirect
ngx.exec("/web2_internal"..ngx.var.request_uri)

GG~

参考:

https://blog.csdn.net/cuichunchi/article/details/89682234

https://blog.csdn.net/reblue520/article/details/100165598

https://www.nginx.com/resources/wiki/modules/lua/#ngx-location-capture

补充:
1,设置proxy_pass_request_headers off;
原因:上传文件的时候,会带上如下请求头:
Content-Type: multipart/form-data; boundary=—-WebKitFormBoundaryEAaYnafq23ZasN
导致子请求的后端通过
file_get_contents(“php://input”)或$_POST都获取不到子请求POST的body数据。
因此需要设置proxy_pass_request_headers off;丢弃客户端的请求头,具体参考:

https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/#ngxlocationcapture

2,上传文件的时候,不让openresty落盘,加上如下设置不做缓存:
proxy_request_buffering off;

补充:
1,给子请求设置请求头,比如传递cookie

local res = ngx.location.capture("/web2/rest/auth.php", {
    method = ngx.HTTP_POST,
    ctx = {},
    vars = {my_Cookie = ngx.var.http_cookie},   -- 添加
    args = {},
    body = '{"username": "admin","password":"a1bc6ce70630870bf20166791197c57eb174eb4e16e36693612b3e499cc115b4","login": true}'
})
location /web2/rest/auth.php {
    set $my_Cookie $my_Cookie;  -- 添加
    proxy_pass_request_headers off;
    # 传递cookie
    proxy_set_header Cookie $my_Cookie;  -- 添加
    proxy_pass https://web2;
}

参考:

https://blog.csdn.net/daiyudong2020/article/details/78572442

https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/#ngxlocationcapture

https://www.csdn.net/tags/NtzacgwsODAyNjctYmxvZwO0O0OO0O0O.html

转载请保留地址:http://www.lenky.info/archives/2022/04/3138http://lenky.info/?p=3138


备注:如无特殊说明,文章内容均出自Lenky个人的真实理解而并非存心妄自揣测来故意愚人耳目。由于个人水平有限,虽力求内容正确无误,但仍然难免出错,请勿见怪,如果可以则请留言告之,并欢迎来讨论。另外值得说明的是,Lenky的部分文章以及部分内容参考借鉴了网络上各位网友的热心分享,特别是一些带有完全参考的文章,其后附带的链接内容也许更直接、更丰富,而我只是做了一下归纳&转述,在此也一并表示感谢。关于本站的所有技术文章,欢迎转载,但请遵从CC创作共享协议,而一些私人性质较强的心情随笔,建议不要转载。

法律:根据最新颁布的《信息网络传播权保护条例》,如果您认为本文章的任何内容侵犯了您的权利,请以Email或书面等方式告知,本站将及时删除相关内容或链接。

分类: *nix技术, nginx 标签: , ,
  1. 本文目前尚无任何评论.
  1. 本文目前尚无任何 trackbacks 和 pingbacks.
您必须在 登录 后才能发布评论.