• 每个后端都应该了解的OpenResty入门以及网关安全实战


    简介

    在官网上对 OpenResty 是这样介绍的(http://openresty.org):

    “OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。”

    “OpenResty 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。”

    “OpenResty 的目标是让你的 Web 服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。”

    从以上官网描述里我们可以知道,OpenResty 官网对其定位是以 Nginx 为核心集成 Lua,打造一个兼具开发效率和高性能的服务端开发平台。

    OpenResty 的核心是基于 Nginx 的一个 C 模块(lua-Nginx-module),该模块将 LuaJIT 嵌入到 Nginx 服务器中,并对外提供一套完整的 Lua API,透明地支持非阻塞 I/O,提供了轻量级线程、定时器等高级抽象。

    我们可以用 Lua 语言来进行字符串和数值运算、查询数据库、发送 HTTP 请求、执行定时任务、调用外部命令等,还可以用 FFI 的方式调用外部 C 函数。这基本上可以满足服务端开发需要的所有功能。

    掌握好了 OpenResty,我们就可以同时拥有脚本语言的开发效率和迭代速度,以及 Nginx C 模块的高并发和高性能优势。

    下面为大家介绍本文大纲

    • OpenResty 的 hello world 该怎么写
    • 快速上手 Lua 脚本语言
    • OpenResty 用到的 Nginx 知识
    • OpenResty 在网关安全中如何应用

    OpenResty 的 hello world 该怎么写

    OpenResty 的安装

    OpenResty 的安装有多种方法,比如使用操作系统的包管理器、源码编译或者 docker 镜像。推荐优先使用 yum、apt-get、brew 这类包管理系统,来安装 OpenResty。

    对于 Mac OS X 或 macOS 用户,强烈推荐您使用 homebrew 包管理工具安装 OpenResty。可以直接使用下面 这一条命令:

    brew install openresty/brew/openresty

    对于一些常见的 Linux 发行版本(Ubuntu、Debian、CentOS、RHEL、Fedora、OpenSUSE、Alpine 和 Amazon Linux), OpenResty 提供 官方预编译包。确保首先用这种方式来安装。这里用 CentOS 举例,可以使用如下方式,

    CentOS 9 或者更新版本

    # add the yum repo:
    wget https://openresty.org/package/centos/openresty2.repo
    sudo mv openresty2.repo /etc/yum.repos.d/openresty.repo
    
    # update the yum index:
    sudo yum check-update

    CentOS 8 或者更老版本

    # add the yum repo:
    wget https://openresty.org/package/centos/openresty.repo
    sudo mv openresty.repo /etc/yum.repos.d/openresty.repo
    
    # update the yum index:
    sudo yum check-update

    然后就可以像下面这样安装软件包,比如 openresty:

    sudo yum install -y openresty

    Docker 安装

    Docker 安装的方式就最为简单了,只需要输入以下命令,就可以获取打包好的镜像。

    docker pull openresty/openresty

    目录结构

    安装 OpenResty 成功后的目录结构如下(以默认安装目录为例):

    /usr/local/openresty/                          #安装主目录
    ├── bin                                     #存放可执行文件
    ├── luajit                                  #LuaJIT运行库
    ├── lualib                                  #Lua组件
    ├── Nginx                                   #Nginx核心运行平台
    ├── pod                                     #参考手册(restydoc)使用的数据
    └── site                                    #包管理工具(opm)使用的数据

    启动服务

    yum 安装完后,就可以直接运行 openresty 命令,启动 OpenResty 服务。

    /usr/local/openresty/bin/openresty         #启动OpenResty服务

    OpenResty 默认开启了 localhost:80 服务,使用 wget 或者 curl 这样的工具就可以验证 OpenResty 是否正常工作:

    curl http://localhost:80                   #curl命令发送HTTP请求

    下面是一些其他常用命令,

    /usr/local/openresty/bin/openresty  -s stop       #停止 OpenResty 服务
    /usr/local/openresty/bin/openresty  -s reload     #重新加载 Nginx 配置文件
    /usr/local/openresty/bin/openresty  -t            #检查 Nginx 配置文件是否正确
    /usr/local/openresty/bin/openresty  -c            #指定配置文件启动

    OpenResty 的操作命令跟 Nginx 保持一致。可以执行 openresty -h 以及 nginx -h 对比看出,

     

    命令行工具 resty

    如果你想安装命令行工具 resty,那么可以像下面这样安装 openresty-resty 包:

    sudo yum install -y openresty-resty

    resty 是一个 cli 工具,可以使用 -e 参数可以在命令行里直接执行 Lua 代码,我们可以在命令行执行如下命令,

    [root@VM-4-5-centos ~]# resty -e "print('hello world')"
    hello OpenResty

    resty 工具还有很多选项用于配置行为,非常灵活,-e 之外较常用的有

    -c                :指定最大并发连接数(默认值是64);
    -I                :指定Lua库的搜索路径;
    -l                :指定加载某个Lua库;
    --http-conf       :定制在http域里的指令;
    --main-include    :定制在main域里的指令;
    --shdict          :定制使用的共享内存(参见10.2节);
    --resolve-ipv6    :允许解析ipv6的地址。

    想了解完整的列表,可以查看 resty -h 命令。

    包管理工具 opm

    跟大多数语言一样有包管理工具一样,OpenResty 也有自己的包管理工具 opm(OpenResty Package Manager),opm 在 openresty-opm 包里,安装命令如下,

    sudo yum install -y openresty-opm

    opm 是 OpenResty 自带的包管理器,在你安装好 OpenResty 之后,就可以直接使用。一些常见用法如下,

    opm search    http                            #搜索关键字http
    opm search    kafka                           #搜索关键字kafka
    opm get       agentzh/lua-resty-http          #安装组件,注意需要sudo
    opm info      agentzh/lua-resty-http          #显示组件的版本、作者等信息
    opm remove    agentzh/lua-resty-http          #移除组件,同样需要sudo
    opm --install-dir=/opt    get xxx             #把组件安装到/opt目录下
    opm --cwd                 get xxx             #安装到当前目录的/resty_modules下

    编写 hello world

    在上文中我们使用命令行工具 resty 写了一个比较简单的 OpenResty 程序,没有 master 进程,也不会监听端口。下面让我们写一个需要启动 OpenResty 服务的 hello world。

    首先找到 OpenResty 安装目录下 nginx/conf/nginx.conf 文件,在 server 下新增 OpenResty 的 content_by_lua 指令,里面嵌入了 ngx.say 的代码:

    server {
            listen       88;
            server_name  localhost;
    
            location / {
                root   html;
                index  index.html index.htm;
            }
    
            location /hello {
                content_by_lua '
                    ngx.say("hello, world")
                ';
            }
    }

    接着我们执行 openresty -s reload 命令,重新加载 nginx.conf 配置文件。没有报错的话,OpenResty 的服务就已经成功启动了。

    最后使用 curl 命令,来查看结果的返回:

    [root@VM-4-5-centos conf]# curl localhost:88/hello
    hello, world

    到这里,一个真正的 OpenResty 开发的 hello world 程序就完成了。

    快速上手 Lua 脚本语言

    Lua 环境

    我们不用专门去安装标准 Lua 5.1 之类的环境,因为 OpenResty 已经不再支持标准 Lua,而只支持 LuaJIT。这里我介绍的 Lua 语法,也是和 LuaJIT 兼容的部分,而不是基于最新的 Lua 5.3,这一点需要特别注意。

    在 OpenResty 的安装目录下,可以找到 LuaJIT 的目录和可执行文件。在 CentOS 系统下,LuaJIT 的目录如下,

    [root@VM-4-5-centos luajit]# cd /usr/local/openresty/luajit/bin/
    [root@VM-4-5-centos bin]# ll
    total 536
    lrwxrwxrwx 1 root root     18 Oct 12 11:22 luajit -> luajit-2.1.0-beta3
    -rwxr-xr-x 1 root root 547728 Jul 18 12:38 luajit-2.1.0-beta3

    我们可以执行 cp luajit /usr/local/bin/ 将 luajit 文件复制到 /usr/local/bin/ 目录下,进而可以直接使用 luajit 命令。

    查看 LuaJIT 的版本号,

    [root@VM-4-5-centos ~]# luajit  -v
    LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2022 Mike Pall. https://luajit.org/

    执行 lua 脚本,

    [root@VM-4-5-centos ~]# echo 'print("hello world")' > 1.lua
    [root@VM-4-5-centos ~]# cat 1.lua
    print("hello world")
    [root@VM-4-5-centos ~]# luajit 1.lua
    hello world
    [root@VM-4-5-centos ~]#

    也可以使用 resty 来直接运行,它最终也是用 LuaJIT 来执行的,

    [root@VM-4-5-centos ~]# resty -e 'print("hello world")'
    hello world

    基本语法

    变量

    在 Lua 中声明变量,可以如下代码所示,

    local a = 'hello'
    b = "world"

    加了 local 关键字,用于声明局部变量。

    不加 local 关键字的话,变量默认是全局的。

    注释

    两个减号是单行注释

    -- 注释

    多行注释

    --[[
     多行注释
     多行注释
     --]]

    行尾结束

    Lua 中代码的行尾结束都不需要添加特殊字符,这跟 Java 不同(Java 在行尾需要添加 ;)。

    local a = 'a'
    print(a)

    数据类型

    Lua 中的数据类型不多,你可以通过 type 函数来返回一个值的类型,比如下面这样的操作:

    [root@VM-4-5-centos ~]# resty -e 'print(type("hello world"))
    >  print(type(print))
    >  print(type(true))
    >  print(type(360.0))
    >  print(type({}))
    >  print(type(nil))
    >  '

    打印如下,

    string
    function
    boolean
    number
    table
    nil

    这几种就是 Lua 中的基本数据类型了。下面我们来简单介绍一下它们。

    字符串

    在 Lua 中,有三种方式可以表达一个字符串:单引号、双引号,以及长括号([[]]),示例如下,

    新建 str.lua 文件,写入以下内容,

    local s = 'a'
    local s1 = "b"
    local s2 = [[c]]
    
    print(s)
    print(s1)
    print(s2)

    执行 luajit str.lua 返回结果如下,

    a
    b
    c

    在 Lua 中,字符串拼接采用 .. 的方式,示例如下,

    编辑 str.lua 文件,写入以下内容,

    local s = 'a'
    local s1 = "b"
    local s2 = [[c]]
    
    print(s)
    print(s1)
    print(s2)
    
    local s3 =s .. s1 ..s2
    print(s3)

    执行 luajit str.lua 返回结果如下,

    a
    b
    c
    abc

    布尔值

    在 Lua 中,只有 nil 和 false 为假,其他都为 true,包括 0 和空字符串也为真。我们可以用示例印证一下:

    新建 bool.lua 脚本文件,写入以下内容,

    local a = 0
    local b
    if a then
      print("true")
    end
    a = ""
    if a then
      print("true")
    end
    
    print(b)

    执行 luajit str.lua 返回结果如下,

    true
    true
    nil

    在 Lua 中,空值就是 nil。如果你定义了一个变量,但没有赋值,它的默认值就是 nil,对应的就是上面示例代码的局部变量 b。

    数字

    Lua 的 number 类型,是用双精度浮点数来实现的。值得一提的是,LuaJIT 支持 dual-number(双数)模式,也就是说,LuaJIT 会根据上下文来用整型来存储整数,而用双精度浮点数来存放浮点数。示例如下,

    新建 number.lua 脚本文件,写入以下内容,

    print(type(2))
    print(type(2.2))
    print(type(0.2))
    print(type(2e+1))
    print(type(0.2e-1))
    print(type(7.8263692594256e-06))
    
    print(2 + 2)
    print(2 + 22.2)

    执行 luajit number.lua 返回结果如下,

    number
    number
    number
    number
    number
    number
    4
    24.2

    函数

    函数在 Lua 中是一等公民,你可以把函数存放在一个变量中,也可以当作另外一个函数的入参和出参。示例如下,

    新建 fun.lua 文件,写入以下代码,

    -- 阶乘
    function factorial1(n)
        if n == 0 then
            return 1
        else
            return n * factorial1(n - 1)
        end
    end
    print(factorial1(5))
    factorial2 = factorial1
    print(factorial2(5))

    执行 luajit fun.lua 返回结果如下,

    120
    120

    分支控制

    Lua 提供了以下两种分支控制结构语句:

    • if 语句
    • if...else 语句
    • if...elseif...else 语句

    if 语句

    Lua if 语句语法格式如下:

    if(布尔表达式)
    then
       --[ 在布尔表达式为 true 时执行的语句 --]
    end

    以下是一个判断变量 a 的值是否小于 20 的示例,

    新建 if1.lua,写入以下内容,

    --[ 定义变量 --]
    a = 10;
    
    --[ 使用 if 语句 --]
    if (a < 20) then
       --[ if 条件为 true 时打印以下信息 --]
       print("a 小于 20" );
    end
    print("a 的值为:", a);

    执行 luajit if1.lua 返回结果如下,

    a 小于 20
    a 的值为: 10

    if...else 语句

    Lua if 语句可以与 else 语句搭配使用, 在 if 条件表达式为 false 时执行 else 语句代码块。

    Lua if...else 语句语法格式如下:

    if(布尔表达式)
    then
       --[ 布尔表达式为 true 时执行该语句块 --]
    else
       --[ 布尔表达式为 false 时执行该语句块 --]
    end

    以下是一个判断变量 a 值的示例,

    新建 if2.lua,写入以下内容,

    --[ 定义变量 --]
    a = 100;
    --[ 检查条件 --]
    if( a < 20 )
    then
       --[ if 条件为 true 时执行该语句块 --]
       print("a 小于 20" )
    else
       --[ if 条件为 false 时执行该语句块 --]
       print("a 大于 20" )
    end
    print("a 的值为 :", a)

    执行 luajit if2.lua 返回结果如下,

    a 大于 20
    a 的值为 : 100

    if...elseif...else 语句

    Lua if 语句可以与 elseif...else 语句搭配使用, 在 if 条件表达式为 false 时执行 elseif...else 语句代码块,用于检测多个条件语句。

    Lua if...elseif...else 语句语法格式如下:

    if( 布尔表达式 1)
    then
       --[ 在布尔表达式 1  true 时执行该语句块 --]
    
    elseif( 布尔表达式 2)
    then
       --[ 在布尔表达式 2  true 时执行该语句块 --]
    
    elseif( 布尔表达式 3)
    then
       --[ 在布尔表达式 3  true 时执行该语句块 --]
    else
       --[ 如果以上布尔表达式都不为 true 则执行该语句块 --]
    end

    以下是一个判断变量 a 值的示例,

    新建 if3.lua,写入以下内容,

    --[ 定义变量 --]
    a = 100
    
    --[ 检查布尔条件 --]
    if( a == 10 )
    then
        --[ 如果条件为 true 打印以下信息 --]
        print("a 的值为 10" )
    elseif( a == 20 )
    then
        --[ if else if 条件为 true 时打印以下信息 --]
        print("a 的值为 20" )
    elseif( a == 30 )
    then
        --[ if else if condition 条件为 true 时打印以下信息 --]
        print("a 的值为 30" )
    else
        --[ 以上条件语句没有一个为 true 时打印以下信息 --]
        print("没有匹配 a 的值" )
    end
    print("a 的真实值为: ", a )

    执行 luajit if3.lua 返回结果如下,

    没有匹配 a 的值
    a 的真实值为:  100

    循环

    Lua 编程语言中 for 循环语句可以重复执行指定语句,重复次数可在 for 语句中控制。

    Lua 编程语言中 for 语句有两大类:

    • 数值 for 循环
    • 泛型 for 循环

    数值 for 循环

    Lua 编程语言中数值 for 循环语法格式:

    for var=exp1,exp2,exp3 do
        <执行体>
    end

    var 从 exp1 变化到 exp2,每次变化以 exp3 为步长递增 var,并执行一次 "执行体"。exp3 是可选的,如果不指定,默认为 1。示例如下,

    新建 for1.lua 文件,写入以下内容,

    function f(x)
        print("function")
        return x*2
    end
    
    for i = 1, f(5) do print(i)
    end

    执行 luajit for1.lua 返回结果如下,

    function
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    泛型 for 循环

    泛型 for 循环通过一个迭代器函数来遍历所有值,类似 java 中的 foreach 语句。

    Lua 编程语言中泛型 for 循环语法格式:

    --打印数组a的所有值
    local a = {"one", "two", "three"}
    for i, v in ipairs(a) do
        print(i, v)
    end

    i 是数组索引值,v 是对应索引的数组元素值。ipairs 是 Lua 提供的一个迭代器函数,用来迭代数组。

    将以上内容下入 for2.lua 文件,打印结果如下,

    1 one
    2 two
    3 three

    Lua 模块与包

    模块类似于一个封装库,从 Lua 5.1 开始,Lua 加入了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。

    Lua 提供了一个名为 require 的函数用来加载模块。要加载一个模块,只需要简单地调用就可以了。例如:

    require("cjson")
    -- 或者
    require "cjson"

    Lua 比较小巧,内置的标准库并不多。在 OpenResty 的环境中默认支持了一些官方模块,如 cjson 可以直接使用,其他的一些第三方库则需要先使用 lua_package_path 指令配置 OpenResty 的文件寻址路径,又或者直接使用 opm 包管理工具来安装一些第三方模块。

    OpenResty 中默认启用了下面列表的绝大部分组件,想要了解更多 OpenResty 相关组件的话,可以翻阅官网说明 https://openresty.org/cn/components.html。

    LuaJIT
    ArrayVarNginxModule
    AuthRequestNginxModule
    CoolkitNginxModule
    DrizzleNginxModule
    EchoNginxModule
    EncryptedSessionNginxModule
    FormInputNginxModule
    HeadersMoreNginxModule
    ...

    本文的 Lua 语法介绍到这里就足够在 OpenResty 中编写 lua 脚本了,想要了解更多 Lua 内容,如 table、文件、调式等可以自行翻阅 https://www.runoob.com/lua/lua-tutorial.html 网站。

    OpenResty 用到的 Nginx 知识

    内置常量和变量

    OpenResty 在内置 Lua 引擎中新增了一些常用的内置变量如下所示。

     

    图片来源https://zhuanlan.zhihu.com/p/539546173

    OpenResty 在内置 Lua 引擎中新增了一些常用的内置常量大致如下所示。

     

     

    这些内置变量和常量都可以在 Lua 脚本中直接使用。

    配置指令

    OpenResty 定义了一系列 Nginx 配置指令,用于配置何时运行用户 Lua 脚本以及如何返回 Lua 脚本的执行结果,这些指令可以直接在 nginx.conf 配置文件中使用。

    OpenResty 定义的 Nginx 配置指令大致如下所示。

     

    图片来源https://zhuanlan.zhihu.com/p/539546173

    这些指令中有 9 个 *_by_lua 指令,它们和 Nginx 的关系如下图所示

     

    图片来自 lua-Nginx-module 文档

    其中,init_by_lua 只会在 Master 进程被创建时执行,init_worker_by_lua 只会在每个 Worker 进程被创建时执行。其他的 *_by_lua 指令则是由终端请求触发,会被反复执行。

    所以在 init_by_lua 阶段,我们可以预先加载 Lua 模块和公共的只读数据,这样可以利用操作系统的 COW(copy on write)特性,来节省一些内存。

    对于业务代码来说,其实大部分的操作都可以在 content_by_lua 里面完成,但更推荐的做法,是根据不同的功能来进行拆分,比如下面这样:

    • set_by_lua:设置变量;
    • rewrite_by_lua:转发、重定向等;
    • access_by_lua:准入、权限等;
    • content_by_lua:生成返回内容;
    • header_filter_by_lua:应答头过滤处理;
    • body_filter_by_lua:应答体过滤处理;
    • log_by_lua:日志记录。

    利用这些阶段的特性,我们可以一些通用逻辑进行拆分处理,比如我们可以在 access 阶段解密,在 body filter 阶段加密就可以了,在 content 阶段的代码是不用做任何修改的。

    # 加密协议版本
    location /test {
        access_by_lua '...';        # 请求体解密
        content_by_lua '...';       # 处理请求,不需要关心通信协议
        body_filter_by_lua '...';   # 应答体加密
    }

    OpenResty 在网关安全中如何应用

    WAF 介绍

    Web 应用防火墙(Web Application Firewall,简称 WAF)对网站或者 App 的业务流量进行恶意特征识别及防护,在对流量清洗和过滤后,将正常、安全的流量返回给服务器,避免网站服务器被恶意入侵导致性能异常等问题,从而保障网站的业务安全和数据安全。

    常见 Web 应用攻击防护

    • 防御一些常见常见威胁:SQL 注入、XSS 跨站、WebShell 上传、后门攻击、命令注入、非法 HTTP 协议请求、常见 Web 服务器漏洞攻击、CSRF、核心文件非授权访问、路径穿越、网站被扫描等。
    • CC 恶意攻击防护:控制单一源 IP 的访问频率,基于重定向跳转验证、人机识别等。针对海量慢速请求攻击,根据统计响应码及 URL 请求分布、异常 Referer 及 User-Agent 特征识别,结合网站精准防护规则综合防护。
    • 网站隐身:不对攻击者暴露站点地址,避免其绕过 Web 应用防火墙直接攻击。

    相关产品

    目前 WAF 相关产品主要有三类:

    • 硬件 WAF:效果好,但是贵!
    • 软件 WAF:效果还算可以,能用,有开源产品!
    • 云厂商 WAF:云厂商的 WAF 都很贵!

    鉴于极客精神(白嫖万岁 ),这里介绍几款业内开源的 WAF 产品,

    • 长亭科技的雷池社区版,主页地址:https://waf-ce.chaitin.cn/
    • ModSecurity,主页地址:https://www.modsecurity.org/
    • Coraza,主页地址:https://coraza.io/
    • VeryNginx,主页地址:https://github.com/alexazhou/VeryNginx
    • NAXSI,主页地址:https://github.com/nbs-system/naxsi
    • NGX_WAF,主页地址:https://github.com/ADD-SP/ngx_waf
    • 南墙,主页地址:https://waf.uusec.com/
    • JANUSEC,主页地址:https://www.janusec.com/
    • HTTPWAF,主页地址:https://github.com/httpwaf/httpwaf2.0
    • 锦衣盾,主页地址:https://www.jxwaf.com/

    对于以上 WAF 产品的一些评价指标如下:

    • 防护效果:主要是两个维度,能不能防住攻击,会不会影响普通用户
    • 技术先进性:防护引擎的技术竞争力,是否具备对抗高级攻击的能力
    • 项目质量:本文将以功能完整性、开源代码质量、文档完整性等角度作为评价依据
    • 社区认可度:反映了项目在用户社区中的声誉和影响力,本文将以 GitHub Star 数作为评价依据
    • 社区活跃度:是潜力的体现,活跃度越高发展越快,本文将以社区用户的参与度和作者维护项目的积极性作为

    最终的的得分如下,

     

    图片来源https://stack.chaitin.com/techblog/detail/115

    需要注意的是软件 WAF 一般在第 7 层中进行防御(osi 模型),并非能够防御所有类型的攻击,比如 ddos 攻击就不能防御。不过一般云厂商提供的 WAF 产品也有携带了 ddos 攻击防御的支持,比如阿里云。

    OpenResty 在 WAF 中的应用

    使用 OpenResty 作为流量入口时,我们可以通过编写一些 Lua 脚本来实现 WAF 防御的功能。Lua 脚本可以在 Nginx 配置文件中指定,在不同的阶段执行。

    对于防火墙功能,我们通常可以在 access_by_lua 阶段执行 Lua 脚本,用于匹配请求或响应的头部或内容,并根据匹配结果决定是否放行数据包或返回错误信息。

    下面我将给大家演示如何使用 OpenResty 实现一个基于 Lua 的 WAF(Web Application Firewall)功能。用来识别和阻止常见的 Web 攻击,如 cc 防御、ip 黑名单、ua 参数校验等。

    cc 防御

    1. 修改 nginx.conf 文件,加入 access_by_lua_file cc.lua 指令,
    http {
      # 声明一个 10m 大小的共享内存 cc_dict
      lua_shared_dict cc_dict 10m;
      lua_package_path "/usr/local/openresty/nginx/conf/lua/waf/?.lua;/usr/local/openresty/lualib/?.lua;";
      ...
      server {
        listen       88;
        server_name  localhost;
    
        # 在access阶段执行 cc 防御插件
        access_by_lua_file cc.lua;
    
        location / {
          ...
        }
      }
    }
    1. 新建 cc.lua 脚本,写入以下内容,
    -- 获取客户端ip
    local function getClientIp()
        IP  = ngx.var.remote_addr
        if IP == nil then
            IP  = "unknown"
        end
        return IP
    end
    
    local function denyCC()
        local uri=ngx.var.uri
        ccCount=100
        ccSeconds=6
        local access_uri = getClientIp()..uri
        local limit = ngx.shared.cc_dict
        local req,_=limit:get(access_uri)
        if req then
            if req > ccCount then
                ngx.exit(503)
                return true
            else
                limit:incr(access_uri,1)
            end
        else
            limit:set(access_uri,1,ccSeconds)
        end
        return false
    end
    
    if denyCC() then
        return
    end
    1. 重启 OpenResty 服务,就完成了 cc 防御功能。
    openresty -s  reload

    ip 黑名单

    1. 修改 nginx.conf 文件,加入 access_by_lua_file ip_block.lua 指令,
    http {
      lua_package_path "/usr/local/openresty/nginx/conf/lua/waf/?.lua;/usr/local/openresty/lualib/?.lua;";
      ...
      server {
        listen       88;
        server_name  localhost;
    
        # 在access阶段执行 ip_block 防御插件
        access_by_lua_file ip_block.lua;
    
        location / {
          ...
        }
      }
    }
    1. 新建 ip_block.lua 脚本,写入以下内容,
    local cjson = require "cjson"
    
    local function read_json(var)
        file = io.open(var,"r")
        if file==nil then
            return
        end
        str = file:read("*a")
        file:close()
        list = cjson.decode(str)
        return list
    end
    
    local function getClientIp()
        IP  = ngx.var.remote_addr
        if IP == nil then
            IP  = "unknown"
        end
        return IP
    end
    
    local function blockIpCheck()
        local ipBlockList=read_json('/usr/local/openresty/nginx/conf/lua/waf/ip_block.json')
        if next(ipBlockList) ~= nil then
            for _,ip in pairs(ipBlockList) do
                if getClientIp()==ip then
                    ngx.exit(403)
                    return true
                end
            end
        end
        return false
    end
    
    if blockIpCheck() then
        return
    end
    1. 在 /usr/local/openresty/nginx/conf/lua/waf 目录下新建 ip_block.json 文件,写入我们要加入黑名单的 ip,
    ["58.48.224.7"]
    1. 重启 OpenResty 服务,就完成了 ip 黑名单功能。
    openresty -s  reload

    ua 拦截

    1. 修改 nginx.conf 文件,加入 access_by_lua_file ua.lua 指令,
    http {
      lua_package_path "/usr/local/openresty/nginx/conf/lua/waf/?.lua;/usr/local/openresty/lualib/?.lua;";
      ...
      server {
        listen       88;
        server_name  localhost;
    
        # 在access阶段执行 ua 防御插件
        access_by_lua_file ua.lua;
    
        location / {
          ...
        }
      }
    }
    1. 新建 ua.lua 脚本,写入以下内容,
    local ngxMatch=ngx.re.match
    local cjson = require "cjson"
    
    local function read_json(var)
        file = io.open(var,"r")
        if file==nil then
            return
        end
        str = file:read("*a")
        file:close()
        list = cjson.decode(str)
        return list
    end
    
    function ua()
        local ua = ngx.var.http_user_agent
        local userAgents=read_json('/usr/local/openresty/nginx/conf/lua/waf/user_agent.json')
        if next(userAgents) ~= nil then
            for _,rule in pairs(userAgents) do
                if rule ~="" and ngxMatch(ua,rule,"isjo") then
                    ngx.exit(403)
                    return true
                end
            end
        end
        return false
    end
    
    if ua() then
        return
    end
    1. 在 /usr/local/openresty/nginx/conf/lua/waf 目录下新建 user_agent.json 文件,写入我们要加入黑名单的 ua 信息,
    ["Chrome/116.0.0.0"]
    1. 重启 OpenResty 服务,就完成了 ua 拦截功能。
    openresty -s  reload

    相关资料

    • OpenResty 官网:https://openresty.org/cn/benchmark.html
    • 菜鸟教程:https://www.runoob.com/lua/lua-tutorial.html
    • 《OpenResty完全开发指南》:https://weread.qq.com/web/bookDetail/fec3240071848696fec3572
    • 《OpenResty从入门到实战》:https://time.geekbang.org/column/intro/186?code=hkx6qkdp47iccvn0yf40aowqzyzzchyykmswfogb90g%3D

    总结

    自此本文介绍了OpenResty入门以及使用 Lua 脚本实现一些常见的网关安全功能等。需要注意的就是大家在已有的 Nginx 服务迁移到 OpenResty 上来时,记得注意 OpenResty 版本,Nginx 与 OpenResty 相同版本情况下,OpenResty 官方是保证完全兼容的。

    最后感谢大家阅读,希望本文能对你有所帮助。

    关注公众号【waynblog】每周分享技术干货、开源项目、实战经验、国外优质文章翻译等,您的关注将是我的更新动力!

  • 相关阅读:
    spring源码 - @Condition原理及运用
    Shell脚本简单认知
    任务三:LTE背景介绍
    【Vue 开发实战】生态篇 # 18:Vuex最佳实践
    uniapp vue3 静态图片引入
    Java+JSP+MySQL基于SSM的智能推荐商城系统-计算机毕业设计
    【C语言】文件操作(万字详解,教你掌握文件操作)
    什么是鉴权?一篇文章带你了解postman的多种方式
    SQL Server的备份和还原
    R语言绘制动态网络图Network教程WGCNA
  • 原文地址:https://www.cnblogs.com/waynaqua/p/17765689.html