原文链接
前言
在前文使用Gateway作为SpringCloud网关中,我们使用接口限流,IP限流等方式一定程度可以防止普通的DoS
攻击,对于更相对更复杂的DDoS
攻击或者极端的Dos
攻击,如果在只应用端进行防御的话效果相对有限
所以当服务器资源允许,我们正常是会在服务器反向代理的位置设置负载均衡,但是这种防御处理的本质还是资源军备竞赛,技术上只是把护甲穿得更均匀不容易有软肋,真的要将防御层级提高数量级,我们一般会在反向代理服务器设置校验。因为反向代理服务器位于离客户端最近,它能较早地识别和拦截恶意请求。这样可以避免恶意流量到达后端应用服务器,从而节省后端资源
比如我们需要允许用户上传大文件,此时需要两个限制,第一限制用户上传文件大小,第二限制用户单日上传文件文件次数,如果从应用端限制,每次请求应用端都需要收到完整的上传文件内容,然后根据配置进行验证直到文件传输完成后才会拒绝请求的流程。这意味着应用端在接收文件时会消耗更多的内存和带宽资源,即使是一个超出限制的文件,即使是已经达到单日上限,它仍然需要先占用内存并且进行一些处理,才会抛出异常。虽然说对于普通使用这些资源可能并不算什么,但是如果伪造用户Token
进行攻击,那么非常多的大文件不断上传,每次接收、判断、拒绝都占用资源,在此场景下的应用端防御是不合理的
但是对于反向单例服务而言,以Nginx
举例,可以在请求进入后端应用之前就检查请求大小。即Nginx
会在接收到请求的初期就解析请求头,并在请求体开始传输之前就能判断文件大小。如果文件大小超过了设置的限制,它会立即拒绝请求,返回 413 Request Entity Too Large
错误,而不需要处理请求体的内容。由于Nginx
只处理了请求的头部信息和流量限制,不需要消耗太多内存,而且阻止了请求继续进入应用端,因此不会占用更多的内存或带宽
实现
这里我们一般是使用Nginx
+Lua
的方式实现,使用nginx -V
检查是否包含--with-http_lua_module
,如果包含可以直接使用,如果不包含则需要添加Lua
模块,一般需要下载Nginx
源码以及安装LuaJIT
库,对于Nginx
源码进行编译,并添加Lua
模块最后安装即可
但是我们这里也可以选择更简单的方式,卸载Nginx
使用OpenResty
完成:
OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。
也就是说,在这里的使用我们可以它认为是一个Nginx Pro
版本,不需要我们重编译Nginx
另加Lua
模块了。当然,OpenResty
还有非常多的额外功能,感兴趣的小伙伴可以自行到官网查阅
安装
以Centos 8
举例,其他操作系统或者发行版本可以参考官网下载安装:
wget https://openresty.org/package/centos/openresty.repo && sudo mv openresty.repo /etc/yum.repos.d/openresty.repo
sudo yum check-update
sudo yum install -y openresty
systemctl enable openresty && systemctl start openresty
最后这里注意需要停止Nginx
,否则端口会冲突报错。然后将原来的Nginx
配置文件复制到openresty
的配置文件夹中重启即可
大小以及单日上传限制
LUA脚本
示例脚本如下:
local redis = require "resty.redis"
local _M = {}
function _M.init_redis()
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect("127.0.0.1", 5525)
if not ok then
ngx.log(ngx.ERR, "Connect fail: ", err)
return nil
end
local ok, err = red:auth("your_passwd")
if not ok then
ngx.log(ngx.ERR, "Auth fail: ", err)
return nil
end
return red
end
function _M.validate_token()
-- Check login
local token = ngx.req.get_headers()["Token-Key"]
if not token then
ngx.log(ngx.ERR, "No token fail")
return false
end
-- Init Redis
local red = _M.init_redis()
if not red then
ngx.log(ngx.ERR, "Redis init fail")
return false
end
-- Token check
local user_key = "Token-Pre:" .. token
local val, err = red:get(user_key)
if val == ngx.null then
ngx.log(ngx.ERR, "Fake token: ", err)
return false
end
-- Check count
local dict = ngx.shared.user_file_upload_counter
local key = "user_file_upload_" .. token .. "_" .. os.date("%Y%m%d")
local count = dict:get(key) or 0
if count >= 5 then
ngx.log(ngx.ERR, "Upload limit")
return false
end
-- Count plus
dict:set(key, count + 1, 86400)
return true
end
return _M
当请求进入脚本后,首先判断token
存在,然后过滤由Redis
过滤,判断真实用户,最后再检查Token
调用次数,当然这里也直接使用Redis
记录调用次数,都是从内存读入所以本质上区别不大,如果使用共享字典实现,那么需要在nginx
配置文件,增加相应声明限制:
http {
lua_shared_dict user_file_upload_counter 5m;
#...
}
Nginx载入脚本
首先我们需要将脚本文件在Nginx
中引用:
http {
lua_package_path "/to/your/path/?.lua;;";
#...
}
注意这里要使用绝对路径,然后在指定路径中使用改脚本:
http {
#...
server {
#...
#...
location /your/specify/path {
client_max_body_size 100M;
access_by_lua_block {
local check_token = require "your_lua_file_name"
local ok = check_token.validate_token()
if not ok then
ngx.status = ngx.HTTP_FORBIDDEN
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
}
proxy_pass http://localhost:5525;
}
location / {
proxy_pass http://localhost:5525;
}
}
}
此时我们调用该接口超过5次就会超过限制,返回403