一. 安装OpenResty

创建OpenResty用户

# useradd -M  www -s /usr/sbin/nologin

安装OpenResty

# apt-get install libpcre3-dev \
    libssl-dev perl make build-essential curl zlib1g-dev -y
# cd /usr/local/src/  && wget https://openresty.org/download/openresty-1.17.8.1rc1.tar.gz
# tar -xf openresty-1.17.8.1rc1.tar.gz
# cd openresty-1.17.8.1rc1
# ./configure --user=www -j2  #不指定--prefix, 默认安装位置在/usr/local/openresty
# make -j2
# make install

创建软连接

# ln -sv /usr/local/openresty/nginx/sbin/nginx  /usr/local/sbin/

启动openresty

# nginx

更多安装方式请阅读官网文档: http://openresty.org/en/installation.html

二. 第一个"hello world"

在OpenResty中写lua代码,主要包含这两步

  1. 修改nginx配置文件,将lua代码嵌入其中
  2. 重载OpenResty使之生效

下面写一个最简单的nginx.conf,在根目录新增content_by_lua_block;,利用ngx.say将“hello,world”打印出来。

user www;
worker_processes  1;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       80;
        location / {
            content_by_lua_block {
                ngx.say("hello world!")
            }
        }
    }
}

检测并重载OpenResty

# 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
# nginx -s reload

如果语法没有报错,并且重载成功,就可以在浏览器或者curl命令来查看返回结果了。

# curl -i 127.0.0.1
HTTP/1.1 200 OK
Server: openresty/1.17.8.1rc1
Date: Fri, 22 May 2020 10:29:58 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive

hello world!

上面打印"hello world" 的方式是直接将lua代码嵌入到nginx配置文件中,我们也可以将lua代码抽离出来,保持代码的可读性和可维护性。

操作其实也很简单。

我们现在/usr/local/openresty/nginx/html目录下创建一个lua目录专门保存lua代码,将ngx.say 写到hello.lua文件中

# cd /usr/local/openresty/nginx/html
# mkdir lua
# cat lua/hello.lua
ngx.say("hello world!")

稍微修改一下上面nginx.conf配置文件,把content_by_lua_block 改成 content_by_lua_file。

user www;
worker_processes  1;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       80;
        location / {
		    default_type 'text/plain';
            content_by_lua_file html/lua/hello.lua;
        }
    }
}

重载OpenResty

# 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
nginx -s reload

使用curl命令来查看返回结果。

# curl -i 127.0.0.1
HTTP/1.1 200 OK
Server: openresty/1.17.8.1rc1
Date: Fri, 22 May 2020 10:32:41 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive

hello world!

content_by_lua_block 语法
content_by_lua_file 语法

三. 收集日志

从这部分开始,我们将一直使用lua代码抽离的方式去完成。

在lua目录创建get_log.lua文件,先尝试获取一下client端的ip地址。

# cat /usr/local/openresty/nginx/html/lua/get_log.lua
local headers = ngx.req.get_headers()
local ip = headers["X-REAL-IP"] or headers["X_FORWARDED_FOR"] or ngx.var.remote_addr or "0.0.0.0"

ngx.say(ip)

在nginx虚拟主机新增一个/log的location,将get_log.lua代码放置在/log下。

        location /log {
		        default_type 'text/plain';
                content_by_lua_file html/lua/get_log.lua;
        }

重载Openresty后用curl测试

# 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
# nginx  -s reload
# curl  -i 127.0.0.1/log
HTTP/1.1 200 OK
Server: openresty/1.17.8.1rc1
Date: Fri, 22 May 2020 10:38:42 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive

127.0.0.1

获取完client端ip后,我们再尝试获取更多的数据,举个栗子,获取url的请求参数和服务器时间。

继续编写get_log.lua代码文件

local dkjson = require "cjson"
local headers = ngx.req.get_headers()
local ip = headers["X-REAL-IP"] or headers["X_FORWARDED_FOR"] or ngx.var.remote_addr or "0.0.0.0"

local uri_args = ngx.req.get_uri_args()
local page_json = {}
if uri_args then
    for key,val in pairs(uri_args) do
        page_json[string.lower(key)] = val
    end
end

page_json["client_ip"] = ip
page_json['server_time'] = ngx.now() * 1000

ngx.say(dkjson.encode(page_json))

检测重载

# curl  -i '127.0.0.1/log?ak=abc&city=北京&name=guoew&age=18'
HTTP/1.1 200 OK
Server: openresty/1.17.8.1rc1
Date: Fri, 22 May 2020 10:47:28 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive

{"client_ip":"127.0.0.1","city":"北京","ak":"abc","name":"guoew","age":"18","server_time":1590144448725}

当OpenResty接收到文件时,如果需要落地到本地磁盘,该怎么处理呢?

先在服务器创建/data/logs目录以存放日志文件。

# mkdir -p /data/logs/ && chown www.www -R /data/logs

继续修改get_log.lua代码文件,新增mylog函数,log文件命名为json_log.log。

local dkjson = require "cjson"
local headers = ngx.req.get_headers()

local log_file = 'json_log.log'

function mylog(msg,log_file)
    local file, err = io.open("/data/logs/" .. log_file,"aw+")
        if file == nil then
            ngx.say(err)

        else
            file:write (msg..'\n')
            file:flush();
            file:close();
        end
end

local ip = headers["X-REAL-IP"] or headers["X_FORWARDED_FOR"] or ngx.var.remote_addr or "0.0.0.0"

local uri_args = ngx.req.get_uri_args()
local page_json = {}
if uri_args then
    for key,val in pairs(uri_args) do
        page_json[string.lower(key)] = val
    end
end

page_json["client_ip"] = ip
page_json['server_time'] = ngx.now() * 1000

mylog(dkjson.encode(page_json),log_file)
ngx.say(dkjson.encode(page_json))

重载OpenResty,使用curl测试,会发现/data/logs/目录下生成json_log.log文件,内容如下

# cat /data/logs/json_log.log
{"client_ip":"127.0.0.1","city":"北京","ak":"abc","name":"guoew","age":"18","server_time":1590144899538}

ngx.req.get_headers用法
ngx.req.get_uri_args用法
ngx.req.get_post_args用法

四. 限流控制

限流控制会根据客户端ip与uri作为校验值进行判断,这部分将会使用到lua_share_dict。限流控制是参考赵班长的 使用Nginx+Lua实现的WAF改编而来。实现了 单个客户端ip访问某一个接口 30s内最多只能访问3次,否则返回403
在/usr/local/openresty/nginx/html/lua/下创建 waf目录,作为限流相关代码的workspace。

# mkdir /usr/local/openresty/nginx/html/lua/waf

在nginx.conf的http context中申请名称为limit,大小为50m的共享内存。并添加waf目录到lua PATH路径中去。

    lua_shared_dict limit 50m;
    lua_package_path "/usr/local/openresty/nginx/html/lua/waf/?.lua;;";

方便日后进行横向扩展(IP黑白名单,URL黑白名单,SQL注入,User-Agent过滤,等等),将代码按功能拆分,编写对应代码,目录结构如下

waf/
├── access.lua   #统一入口脚本
├── config.lua   #配置开关
├── init.lua     #初始化函数
└── lib.lua      #依赖函数

对应代码如下
config.lua

--WAF config file,enable = "on",disable = "off"

-- Define waf switch
config_waf_enable = "on"
-- Define cc switch
config_cc_check = "on"
-- Define cc rate(CCcount/CCseconds)
config_cc_rate = "3/30"

lib.lua

--Get the client IP
function get_client_ip()
    local headers = ngx.req.get_headers()
    local CLIENT_IP = headers["X-REAL-IP"] or headers["X_FORWARDED_FOR"] or ngx.var.remote_addr
    if CLIENT_IP == nil then
        CLIENT_IP  = "unknown"
    end
    return CLIENT_IP
end

--Get the client user agent
function get_user_agent()
    local USER_AGENT = ngx.var.http_user_agent
    if USER_AGENT == nil then
        USER_AGENT = "unknown"
    end
    return USER_AGENT
end

--WAF log record for json,(use logstash codec => json)
function log_record(method,url,data,ruletag)
    local cjson = require("cjson")
    local io = require 'io'
    local LOG_PATH = "/data/logs/"
    local CLIENT_IP = get_client_ip()
    local USER_AGENT = get_user_agent()
    local SERVER_NAME = ngx.var.server_name
    local LOCAL_TIME = ngx.localtime()
    local log_json_obj = {
        client_ip = CLIENT_IP,
        local_time = LOCAL_TIME,
        server_name = SERVER_NAME,
        user_agent = USER_AGENT,
        attack_method = method,
        req_url = url,
        req_data = data,
        rule_tag = ruletag,
    }
    local LOG_LINE = cjson.encode(log_json_obj)
    local LOG_NAME = LOG_PATH..'/'..ngx.today().."_waf.log"
    local file, err = io.open(LOG_NAME,"aw+")
    if file == nil then
        return

    else
        file:write(LOG_LINE.."\n")
        file:flush()
        file:close()
    end
end

access.lua

require "init"

local function waf_main()
    if cc_attack_check() then
    else
        return
    end
end

-- main
waf_main()

init.lua

require 'lib'
require 'config'

--deny cc attack
function cc_attack_check()
    if config_cc_check == "on" then
        local ATTACK_URI = ngx.var.uri
        local CC_TOKEN = get_client_ip() .. ATTACK_URI
        local limit = ngx.shared.limit
        local CCcount=tonumber(string.match(config_cc_rate,'(.*)/'))
        local CCseconds=tonumber(string.match(config_cc_rate,'/(.*)'))
        local req,_ =  limit:get(CC_TOKEN)
        if req then
            if req >= CCcount then
                log_record('CC_Acttack',ngx.var.request_uri,"-","-")
                if config_waf_enable == "on" then
                    ngx.exit(403)
                end

            else
                limit:incr(CC_TOKEN,1)
            end

        else
            limit:set(CC_TOKEN,1,CCseconds)
        end
    end
    return
end

在nginx.conf 中http context 添加初始化和入口脚本。截止当前,如下是nginx.conf所有的配置。

user www;
worker_processes  1;
events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;


    lua_shared_dict limit 50m;
    lua_package_path "/usr/local/openresty/nginx/html/lua/waf/?.lua;;";
    init_by_lua_file "/usr/local/openresty/nginx/html/lua/waf/init.lua";
    access_by_lua_file "/usr/local/openresty/nginx/html/lua/waf/access.lua";

    server {
        listen       80;
        location / {
            default_type 'text/plain';
            content_by_lua_file html/lua/hello.lua;
        }
        location /log {
                default_type 'text/plain';
                content_by_lua_file html/lua/get_log.lua;
        }
    }
}

重启nginx使之生效,然后使用curl进行10次测试,会发现同一个url地址在访问第四次时,直接返回403。

# for i in `seq 1 10` ; do curl -I 127.0.0.1/log  2>/dev/null | awk '/^HTTP/{print $2}' ; done
200
200
200
403
403
403
403
403
403
403

在这里再解释一下限流的功能,单个客户端ip访问某一个接口 30s内最多只能访问3次,否则返回403,也就是说该限流限制的是访问接口的频次,而非访问服务端域名的频次。

当客户端超过限制时,如果感觉返回403不太友好,也可以自定义内容,或者考虑重定向到其他页面。下面是重定向到 阿拉丁指数 首页的一段伪代码。

...
                if config_waf_enable == "on" then
                    ngx.redirect('https://www.aldzs.com')
                    --ngx.exit(403)
                end
...

lua_share_dict 用法
init_by_lua_file 用法
access_by_lua_file 用法
ngx.redirect 用法

通俗易懂 限流算法原理剖析

五. 白名单

可参考使用Nginx+Lua实现的WAF

六. 灰度发布

灰度发布demo是基于客户端IP来实现的,是参考Openresty+Lua+Redis灰度发布 完成。流程图如下,在管理后台设置灰度IP名单,允许一部分用户(灰度IP名单)访问预发布环境,其他用户则访问原有生产环境。

OpenResty应用实践-LMLPHP

执行过程:

  1. 当用户请求到达前端web(代理)服务器Openresty,内嵌的lua模块解析Nginx配置文件中的lua脚本代码;
  2. Lua获取客户端IP地址,去查询Redis中是否有该键值,如果有返回值执行@pre,否则执行@pro。
  3. Location @pre把请求转发给预发布服务器,location @pro把请求转发给生产服务器,服务器返回结果,整个过程完成。

安装redis-server

# apt install redis-server -y

OpenResty部分配置如下

user www;
worker_processes  1;
events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    keepalive_timeout  65;

    upstream pro {
        server 127.0.0.1:81; #模拟生产环境
    }

    upstream pre {
        server 127.0.0.1:82; #模拟预发布环境
    }

    lua_shared_dict limit 50m;
    lua_package_path "/usr/local/openresty/nginx/html/lua/waf/?.lua;;";
    init_by_lua_file "/usr/local/openresty/nginx/html/lua/waf/init.lua";
    access_by_lua_file "/usr/local/openresty/nginx/html/lua/waf/access.lua";

    server {
        listen       80;
        location /gray {
                default_type 'text/plain';
                content_by_lua_file html/lua/gray.lua ;
        }

        location @pro {
            proxy_pass http://pro;
        }
        location @pre {
            proxy_pass http://pre;
        }
    }
    server {
        listen 81;
        default_type 'text/plain';
        add_header Content-Type 'text/html; charset=htf-8';
        return 200 "<h1>This is pro</h1>" ;
    }
    server {
        listen 82;
        default_type 'text/plain';
        add_header Content-Type 'text/html; charset=htf-8';
        return 200 "<h1>This is pre</h1>";
    }

    error_log /data/logs/error.log debug ;
}

在/usr/local/openresty/nginx/html/lua/下编写 gray.lua脚本,内容如下

require "lib"

local redis = require "resty.redis"
local red = redis:new()

red:set_timeouts(1000, 1000, 1000) -- 1 sec

local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    ngx.say("failed to connect: ", err)
    return
end

local local_ip = get_client_ip()
local intercept = red:get(local_ip)

if intercept == local_ip then
    ngx.exec("@pre")
    return
end
ngx.exec("@pro")

local ok, err = red:close()

if not ok then
    ngx.say("failed to close:", err)
    return
end

在redis里set本机回环ip的键值对,使用curl进行测试

# redis-cli
127.0.0.1:6379> set 127.0.0.1 127.0.0.1
OK
127.0.0.1:6379> exit
root@VM-0-2-ubuntu:/usr/local/openresty/nginx/html/lua# curl  -i 127.0.0.1/gray
HTTP/1.1 200 OK
Server: openresty/1.17.8.1rc1
Date: Wed, 27 May 2020 09:10:27 GMT
Content-Type: text/plain
Content-Length: 11
Connection: keep-alive

<h1>This is pre</h1>

通过其他服务器进行curl测试

# curl -i  118.24.64.250/gray
HTTP/1.1 200 OK
Server: openresty/1.17.8.1rc1
Date: Wed, 27 May 2020 09:11:21 GMT
Content-Type: text/plain
Content-Length: 11
Connection: keep-alive

<h1>This is pro</h1>

为了方便进行测试验证,在118.24.64.250这个web服务,增加了一个/set接口,可以直接将客户端IP设置到redis中,过期时间15s。测试如下

# curl   118.24.64.250/set ; curl  118.24.64.250/gray ; sleep 16 ; curl  118.24.64.250/gray
{"code": 200,"message": "This key(182.254.208.xxx) is set successfully!"}
<h1>This is pre</h1>
<h1>This is pro</h1>

lua-resty-redis 用法
ngx.exec 用法


END

05-29 00:34