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

本文涉及的产品
Web应用防火墙 3.0,每月20元额度 3个月
简介: 泛型 for 循环通过一个迭代器函数来遍历所有值,类似 java 中的 foreach 语句。Lua 编程语言中泛型 for 循环语法格式:

数据类型

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

shell

复制代码

[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))
>  '

打印如下,

typescript

复制代码

string
function
boolean
number
table
nil

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

字符串

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

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

lua

复制代码

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

复制代码

a
b
c

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

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

lua

复制代码

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

复制代码

a
b
c
abc

布尔值

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

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

lua

复制代码

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

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

go

复制代码

true
true
nil

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

数字

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

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

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 返回结果如下,

typescript

复制代码

number
number
number
number
number
number
4
24.2

函数

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

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

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 语句语法格式如下:

scss

复制代码

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

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

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

lua

复制代码

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

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

css

复制代码

a 小于 20
a 的值为:  10

if...else 语句

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

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

arduino

复制代码

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

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

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

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 返回结果如下,

css

复制代码

a 大于 20
a 的值为 : 100

if...elseif...else 语句

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

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

scss

复制代码

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

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

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

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 返回结果如下,

css

复制代码

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

循环

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

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

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

数值 for 循环

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

scala

复制代码

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

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

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

lua

复制代码

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

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

bash

复制代码

function
1
2
3
4
5
6
7
8
9
10

泛型 for 循环

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

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

lua

复制代码

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

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

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

sql

复制代码

1 one
2 two
3 three
3	three

Lua 模块与包

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

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

lua

复制代码

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

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

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

erlang

复制代码

LuaJIT
ArrayVarNginxModule
AuthRequestNginxModule
CoolkitNginxModule
DrizzleNginxModule
EchoNginxModule
EncryptedSessionNginxModule
FormInputNginxModule
HeadersMoreNginxModule

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

OpenResty 用到的 Nginx 知识

内置常量和变量

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

image.png

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

image.png

image.png


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

配置指令

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

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

image.png

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

image.png

其中,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 阶段的代码是不用做任何修改的。

bash

复制代码

# 加密协议版本
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 产品,

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

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

最终的的得分如下,

需要注意的是软件 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 指令,

nginx

复制代码

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 脚本,写入以下内容,

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 指令,

nginx

复制代码

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 脚本,写入以下内容,

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,

css

复制代码

["58.48.224.7"]

4.  重启 OpenResty 服务,就完成了 ip 黑名单功能。

复制代码

openresty -s  reload

ua 拦截

  1. 修改 nginx.conf 文件,加入 access_by_lua_file ua.lua 指令,

nginx

复制代码

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 脚本,写入以下内容,

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 信息,

css

复制代码

["Chrome/116.0.0.0"]

4.  重启 OpenResty 服务,就完成了 ua 拦截功能。

复制代码

openresty -s  reload

相关资料

总结

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

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

目录
相关文章
|
8天前
|
存储 SQL 数据库
深入浅出后端开发之数据库优化实战
【10月更文挑战第35天】在软件开发的世界里,数据库性能直接关系到应用的响应速度和用户体验。本文将带你了解如何通过合理的索引设计、查询优化以及恰当的数据存储策略来提升数据库性能。我们将一起探索这些技巧背后的原理,并通过实际案例感受优化带来的显著效果。
27 4
|
15天前
|
运维 NoSQL Java
后端架构演进:微服务架构的优缺点与实战案例分析
【10月更文挑战第28天】本文探讨了微服务架构与单体架构的优缺点,并通过实战案例分析了微服务架构在实际应用中的表现。微服务架构具有高内聚、低耦合、独立部署等优势,但也面临分布式系统的复杂性和较高的运维成本。通过某电商平台的实际案例,展示了微服务架构在提升系统性能和团队协作效率方面的显著效果,同时也指出了其带来的挑战。
55 4
|
18天前
|
JavaScript API 开发工具
<大厂实战场景> ~ Flutter&鸿蒙next 解析后端返回的 HTML 数据详解
本文介绍了如何在 Flutter 中解析后端返回的 HTML 数据。首先解释了 HTML 解析的概念,然后详细介绍了使用 `http` 和 `html` 库的步骤,包括添加依赖、获取 HTML 数据、解析 HTML 内容和在 Flutter UI 中显示解析结果。通过具体的代码示例,展示了如何从 URL 获取 HTML 并提取特定信息,如链接列表。希望本文能帮助你在 Flutter 应用中更好地处理 HTML 数据。
99 1
|
26天前
|
缓存 架构师 数据库
后端开发的艺术:从入门到精通的旅程####
本文旨在探索后端开发的本质与魅力,通过一段段深入浅出的故事,串联起后端技术的精髓。不同于传统的技术总结,这里我们将以一位普通开发者的成长轨迹为线索,展现从初识编程到成为后端架构师的心路历程。每个阶段都伴随着挑战、学习与突破,最终揭示了技术背后的人文关怀与创新精神。 ####
|
1月前
|
安全 Java 关系型数据库
探索后端技术:构建高效、安全的应用服务
在当今数字化浪潮中,后端技术作为软件架构的核心支柱,承载着处理数据逻辑、保障应用性能与安全等关键任务。本文旨在深入浅出地探讨后端开发的重要概念、主流技术栈以及未来发展趋势,为开发者提供一份指南,以助力构建既高效又安全的应用程序。
|
16天前
|
监控 API 持续交付
后端开发中的微服务架构:从入门到精通
【10月更文挑战第26天】 在当今的软件开发领域,微服务架构已经成为了众多企业和开发者的首选。本文将深入探讨微服务架构的核心概念、优势以及实施过程中可能遇到的挑战。我们将从基础开始,逐步深入了解如何构建、部署和管理微服务。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的见解和实用的建议。
35 0
|
18天前
|
JSON Dart 数据格式
<大厂实战场景> ~ flutter&鸿蒙next处理后端返回来的数据的转义问题
在 Flutter 应用开发中,处理后端返回的数据是常见任务,尤其涉及转义字符时。本文详细探讨了如何使用 Dart 的 `dart:convert` 库解析包含转义字符的 JSON 数据,并提供了示例代码和常见问题的解决方案,帮助开发者有效处理数据转义问题。
113 0
|
19天前
|
前端开发 JavaScript NoSQL
探索后端开发之旅:从基础到高级实战
【10月更文挑战第24天】在这个数字时代的浪潮中,后端开发如同一座巨大的宝藏岛,等待着勇敢的探险者去发掘。本文将作为你的藏宝图,引领你从浅滩走向深海,探索后端开发的广阔天地。无论你是初心者还是资深开发者,这篇文章都将为你提供价值连城的知识和技能。准备好了吗?让我们启航,一起构建强大、高效、安全的后端系统!
|
1月前
|
JavaScript 前端开发
vue3教程,如何手动获取后端数据(入门到精通3,新人必学篇)
本文提供了一个Vue 3教程,讲解了如何使用axios库手动从后端获取数据,包括安装axios、配置后端访问地址、编写路由地址、发起HTTP请求以及在组件中读取和打印响应数据的步骤。
311 0
vue3教程,如何手动获取后端数据(入门到精通3,新人必学篇)
|
2月前
|
Rust API Go
API 网关 OpenID Connect 实战:单点登录(SSO)如此简单
单点登录(SSO)可解决用户在多系统间频繁登录的问题,OIDC 因其标准化、简单易用及安全性等优势成为实现 SSO 的优选方案,本文通过具体步骤示例对 Higress 中开源的 OIDC Wasm 插件进行了介绍,帮助用户零代码实现 SSO 单点登录。