利用OpenResty解决IP反作弊

广告投放项目中Nginx起初有通过ngx_http_limit_req_modulengx_stream_limit_conn_module配置来限制单个IP在同一时间段的访问和并发次数来防止CC攻击,但在解决一些作弊流量上有些力不从心了。

比如有些IP的在没有展示或者展示时间过短的的情况下,模拟客户端(点击触摸参数宏替换)请求广告点击链接,之前这些非法流量是在离线处理才会过滤掉,没有及时的禁止访问,直接导致这些流量可能会被上游DSP反作弊命中影响收入,所以最近着手处理这个问题。

目前的解决思路是,首先收集到异常IP列表入Redis并设置过期时间,在Nginx上利用Lua判断当前IP是否在黑名单中,从而达到反作弊功能。

确定思路后,在测试环境搭建环境,Google下找到几个方案,对比之后感觉 OpenResty 可以满足,里面包含了需要的Nginx Lua Redis模块,且文档比较详细。

具体的安装方法,在 OpenResty 有详细的说明,下面简单列出来。

1
2
3
4
5
6
7
$ yum install readline-devel pcre-devel openssl-devel gcc
$ wget https://openresty.org/download/openresty-1.9.15.1.tar.gz
$ tar xvf openresty-1.9.15.1.tar.gz
$ cd openresty-1.9.15.1
$ ./configure
$ make
$ make install

安装之后,会在 /usr/local/openresty 下面生成相关的文件,下面会有一个 /usr/local/openresty/nginx/sbin/nginx,我们可以使用这个来做代理。

修改nginx.conf来让Lua大施拳脚。

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
    listen      80;
    server_name xxx;
    location /lua {
            default_type 'text/plain';
            content_by_lua 'ngx.say("hello, lua")';
    }
    location / {
            proxy_pass http://127.0.0.1:8080;
            access_log off;
            access_by_lua_file /opt/script/lua/block_ip.lua;
    }
}

启动Nginx,访问//xxx/lua,响应中出现hello, lua说明配置生效,接下来把脚本放到指定目录就应该可以来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local redis = require "resty.redis"
local cache = redis.new()
local ok , err = cache.connect(cache,"127.0.0.1","6381")
cache:set_timeout(600)

if not ok then
  goto Lastend
end

is_forbidden, err = cache:get("engine:block:ip:"..ngx.var.remote_addr)

if is_forbidden == '1' then
  ngx.exit(ngx.HTTP_FORBIDDEN)
  goto Lastend
end

::Lastend::
local ok, err = cache:close()

异常IP收集入Redis后,访问返回403,IP的key过期后恢复访问,简单的反作弊算是基本完成了,下一步了继续优化。


[2016-07-04]更新:

考虑到Redis单点压力,决定利用Nginx共享来做多级缓存,Lua脚本调整后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
local redis = require "resty.redis"

--nginx.conf 配置lua_shared_dict RBL 64m;
local cache_ngx = ngx.shared.RBL

--查询ip是否在封禁段内,若在则返回403错误代码
local key = "engine:block:ip:"..ngx.var.remote_addr
local value = cache_ngx:get(key)
if value == '1' then
	ngx.log(ngx.WARN, "被Nginx缓存命中拦截的IP: " .. key)
	ngx.exit(ngx.HTTP_FORBIDDEN)
else
	local cache_rds = redis.new()
	local ok , err = cache_rds.connect(cache_rds, "127.0.0.1", "6381")
	if not ok then
		ngx.log(ngx.WARN, "Redis获取链接异常: " .. err)
  	else
  		cache_rds:set_timeout(600)
		value = cache_rds:get(key)
		if value == '1' then
			ngx.log(ngx.WARN, "被Redis缓存命中拦截的IP: " .. key)
			--回写nginx,设置expires2分钟
			cache_ngx:set(key, value, 120)
			ngx.exit(ngx.HTTP_FORBIDDEN)
		end
	end
	local ok, err = cache_rds:close()
end