利用Nginx构建简易大文件上传预防DoS攻击机制

原文链接

欢迎大家对于本站的访问 - AsterCasc

前言

在前文使用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

参考资料

OpenResty

原文链接

欢迎大家对于本站的访问 - AsterCasc

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值