一. 安装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}

我们也可以获取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('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名单)访问预发布环境,其他用户则访问原有生产环境。

执行过程:

  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

OpenResty应用实践的更多相关文章

  1. OpenResty 最佳实践 lua与nginx的结合 --引用自https://moonbingbing.gitbooks.io/openresty-best-practices/content/

    系统的说明了lua在nginx上的开发 请大家到源址查看 OpenResty最佳实践

  2. OpenResty 最佳实践 1

    建议先搜索<OpenResty最佳实践.pdf> 到网上下载openresty-1.13.6.1-win32 考虑到操作方便性,建议建立个bin目录,放入系统目录中,生成 nginx-st ...

  3. OpenResty 最佳实践 (2)

    此文已由作者汤晓静授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. lua 协程与 nginx 事件机制结合 文章前部分用大量篇幅阐述了 lua 和 nginx 的相关知识,包 ...

  4. OpenResty 最佳实践

    OpenResty 最佳实践 https://moonbingbing.gitbooks.io/openresty-best-practices/content/index.html

  5. OpenResty 最佳实践 (1)

    此文已由作者汤晓静授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. OpenResty 发展起源 OpenResty(也称为 ngx_openresty)是一个全功能的 Web ...

  6. 《OpenResty 最佳实践》学习开篇

    前言:对openresty学习中,收集了一些相关知识的参考网站,有兴趣的可以看看.另附网盘分享. lua菜鸟教程 openresty最佳实战 lua在线解析工具 Nginx Lua API Nginx ...

  7. 转: OpenResty最佳实践

    https://moonbingbing.gitbooks.io/openresty-best-practices/content/ centOS安装另加内容 ln -sf luajit-2.1.0- ...

  8. 转:OpenResty最佳实践(推荐了解lua语法)

    看点: 1. Lua 语法的说明, 排版清晰易懂. 转: https://moonbingbing.gitbooks.io/openresty-best-practices/content/lua/m ...

  9. OpenResty最佳实践

    https://moonbingbing.gitbooks.io/openresty-best-practices/content/

随机推荐

  1. Centos7网络配置( 网关、dns、ip地址配置)

    0.前提:设置VMware的虚拟网络编辑器 子网和网关设置   虚拟网络编辑器 1.配置DNS(可忽略) $vim /etc/resolv.conf nameserver 192.168.0.1 2. ...

  2. Notations

    下面四种记号是为了建立函数间的相对级别. CLRS上的一张图很直观: 大O记号 定义:如果存在正常数\(c\)和\(n_0\),使得当\(N\ge n_o\)时\(T(N)\le cf(N)\),记\ ...

  3. RF(表格数据获取)

    一.表格数据获取 Get Table Cell [locator | row | col] 获取表格数据 Open Browser file:///D:/HBuilderX/workspace/Pro ...

  4. 使用docker搭建自己的博客(一)

    购买服务器 首先服务器选择腾讯云学生服务器,25岁以下实名认证后月租10块,还是很适合我这种简约派的 又财大气粗买了个一年的域名,后面涨价再说吧 安装docker 使用xshell连上服务器 安装必要 ...

  5. shell之路 shell核心语法【第三篇】运算

    Bash 支持很多运算符,包括算数运算符.关系运算符.布尔运算符.字符串运算符和文件测试运算符. 原生bash不支持简单的数学运算,默认都是字符串操作,但是可以通过其他命令来实现 算数运算 expr. ...

  6. Edge Weight Assignment(树-异或-贪心)

    大意: 给定一棵无根树,要求你任意设置n-1条边的边权. 使得任意叶子节点间边权的XOR值为0: 此时,令f为所有边权数值不同的个数,求最小的f和最大的f. \(\color{Red}{------- ...

  7. N - Subpalindromes URAL - 1989 哈希+线段树

    N - Subpalindromes URAL - 1989 这个是一个哈希+线段树,这个题目也不算特别难,但是呢,还比较有意思. 这个题目给你两个操作,一个是回答l~r 区间是不是回文,一个是对一个 ...

  8. springboot设置过滤器、监听器、拦截器

    其实这篇文章算不上是springboot的东西,我们在spring普通项目中也是可以直接使用的 设置过滤器: 以前在普通项目中我们要在web.xml中进行filter的配置,但是只从servlet 3 ...

  9. Spring Cloud学习 之 Spring Cloud Ribbon 重试机制及超时设置不生效

    今天测了一下Ribbon的重试跟超时机制,发现进行的全局超时配置一直不生效,配置如下: ribbon: #单位ms,请求连接的超时时间,默认1000 ConnectTimeout: 500 #单位ms ...

  10. 【HBase】通过Java代码实现HBase数据库中数据的增删改查

    目录 创建maven工程,导入jar包 java代码实现创建hbase表 java代码实现向hbase表中插入数据 java代码查询hbase数据 使用rowKey查询指定列族指定列的值 通过star ...