OpenResty應用實踐
一. 安裝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 //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
更多安裝方式請閱讀官網文檔: //openresty.org/en/installation.html
二. 第一個”hello world”
在OpenResty中寫lua代碼,主要包含這兩步
- 修改nginx配置文件,將lua代碼嵌入其中
- 重載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}
我們也可以獲取POST方式請求的data信息,利用
ngx.req.get_post_args
方法,具體實現就不在這裡寫了。
當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('//www.aldzs.com')
--ngx.exit(403)
end
...
lua_share_dict 用法
init_by_lua_file 用法
access_by_lua_file 用法
ngx.redirect 用法
五. 白名單
六. 灰度發佈
灰度發佈demo是基於客戶端IP來實現的,是參考Openresty+Lua+Redis灰度發佈 完成。流程圖如下,在管理後台設置灰度IP名單,允許一部分用戶(灰度IP名單)訪問預發佈環境,其他用戶則訪問原有生產環境。
執行過程:
- 當用戶請求到達前端web(代理)服務器Openresty,內嵌的lua模塊解析Nginx配置文件中的lua腳本代碼;
- Lua獲取客戶端IP地址,去查詢Redis中是否有該鍵值,如果有返回值執行@pre,否則執行@pro。
- 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 //pro;
}
location @pre {
proxy_pass //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