流量隔离
流量管控
流量消峰
流量限流
降级处理
热点数据处理
容灾处理
在上篇中我已经介绍过 Nginx 限流的语法了,现在咱们就直接来实践。这里呢,我们是根据用户 ID 来做限流防刷的。
首先我们新建一个通用配置文件 common.conf,用来定义限流规则以及后续一些其他的通用配置,所在位置如下:

同时,不要忘记在 nginx.conf 文件中将该配置文件引入进去,和引入 upstream.conf 一样。然后,我们在 common.conf 中定义限流规则如下:
# limit by userlimit_req_zone $user_id zone=limit_by_user:10m rate=1r/s;
意为定义了一个名为 limit_by_user 的限流规则,根据用户 ID 来做限流,限流的速率为同一个用户 1 秒内只允许 1 个请求通过,且为该规则申请的内存大小为 10M。
这里的 10M 大概是什么概念呢?可以简单粗略地算下,假如一个 user_id 占用的内存大小为 16 字节,那么 10M 的内存大概可以处理单机 10*1024*1024/16=655360 个请求。
规则配置完毕后,接下来就在我们需要限流的接口引用该规则,这里依然以活动查询接口为例,配置如下:
#活动数据查询location activity/query{limit_req zone=limit_by_user nodelay;default_type text/plain;proxy_pass http://backend;}
其中 nodelay 是被限流后的策略,意为不等待,直接返回。
配置好之后,我们启动 Nginx,通过 URL 进入到商详页(要在活动进行中时)。

这时,我们通过鼠标快速地刷新两次页面(点击浏览器中的刷新图标,在 1 秒内完成),来模拟外部的请求,然后看下对应的 access 和 error 日志,结果如下:

通过 domain-access.log 日志可以看到,两次请求,第一次正常返回,第二次返回给客户端 503 的状态码,原因通过 domain-error.log 可以看到,是触发了根据用户 ID 的限流规则,这样我们的限流防刷功能就实现了。
以上通过限流的方式来防刷,是非常简单且直接的一种方式,这种方式可以有效解决黑产流量对单个接口的高频请求,但要想防止刷子不经过前置流程直接提单,还需要引入一个流程编排的 Token 机制。
Token 我想你是知道的,一般都是用来做鉴权的。放到秒杀的业务场景就是,对于有先后顺序的接口调用,我们要求进入下个接口之前,要在上个接口获得令牌,不然就认定为非法请求。同时这种方式也可以防止多端操作对数据的篡改,如果我们在 Nginx 层做 Token 的生成与校验,可以做到对业务流程主数据的无侵入。
在 Token 机制下,前端与 seckill-nginx 中的接口交互时序图如下所示:

现在我们就按照对应的时序,依次给 4 个接口增加上 Token 相关的生成与校验功能。
在这之前,我们为了更真实地获取用户 ID,需要在 seckill-web 中新增加个登录功能,模拟将 user_id 放到 cookie,这样之后的每次请求,我们就直接从 cookie 中获取 user_id 即可。
同时根据我们的设计,有几个主要参数是每次接口请求都必须要校验的,那就是用户 ID、产品编号,还有新的 Token,所以我们就定义了一个统一的解析方法,其中用户 ID 从 cookie 中解析,产品编号和 st 从请求的 URL 中解析,如下所示:
#security tokenset $st "";#产品编号set $product_id "";#用户IDset_by_lua_file $user_id Users/Eleven/idea-practic/seckill-nginx/lua/set_common_var.lua;
set_common_var.lua 主要就是负责参数的解析,并给对应的变量做赋值,如下所示:
-- 解析通用变量赋值功能--通过请求URL获取st,并赋值给变量stlocal param_st = ngx.var.arg_stif not param_st thenparam_st = ""endngx.var.st = param_st--通过请求URL获取产品编号,并赋值给变量product_idlocal param_product_id = ngx.var.arg_productIdif not param_product_id thenparam_product_id = ""endngx.var.product_id = param_product_id--通过cookie获取用户ID,并赋值给user_idlocal user_id = ngx.var.cookie_user_idif not user_id thenuser_id = ""endreturn user_id
做完之后,我们开始改造活动数据查询接口,改造后如下:
#活动数据查询location activity/query{limit_req zone=limit_by_user nodelay;content_by_lua_file Users/Eleven/idea-practic/seckill-nginx/seckill-nginx/lua/activity_query.lua;#设置返回的header,并将security token放在header中header_filter_by_lua_block{ngx.header["st"] = ngx.md5(ngx.var.user_id.."1")--这里为了解决跨域问题设置的,不存在跨域时不需要设置以下headerngx.header["Access-Control-Expose-Headers"] = "st"ngx.header["Access-Control-Allow-Origin"] = "http://localhost:8080"ngx.header["Access-Control-Allow-Credentials"] = "true"}}
这里通过 header_filter_by_lua_block 指令,在返回的 header 里增加流程 Token。这里 st 的生成只是简单地将用户 ID+ 步骤编号做了 MD5,生产上需要更严格一些,需要加入商品编号、活动开始时间、自定义加密 key 等,这样前端通过解析请求响应 header,就可以拿到 st 了,然后将其拼在请求结算页 H5 的 URL 后即可。
然后再对结算页 H5 的 location 做改造,改造后如下图所示(当然这里可以将 rewrite_by_lua 的内容放到 file,看起来会更整洁一些):
#进结算页页面(H5)location settlement/prePage{default_type text/html;rewrite_by_lua_block{--校验活动查询的stlocal _st = ngx.md5(ngx.var.user_id.."1")--校验不通过时,以500状态码,返回对应错误页if _st ~= ngx.var.st thenngx.log(ngx.ERR,"st is not valid!!")return ngx.exit(500)end--校验通过时,再生成个新的st,用于下个接口校验local new_st = ngx.md5(ngx.var.user_id.."2")--ngx.exec执行内部跳转,浏览器URL不会发生变化--ngx.redirect(url,status) 其中status为301或302local redirect_url = "/settlement/page".."?productId="..ngx.var.product_id.."&st="..new_streturn ngx.redirect(redirect_url,302)}error_page 500 502 503 504 html_fail.html;}
结算页 H5 的改造点,就是将之前的一个 location 拆成了 2 个了,增加了 settlement/prePage,这样点击立即抢购时就可以直接调用这个接口。
这里你可能会有疑问,这里为什么没有选择将 st 放在 header 中返回呢?因为和活动数据查询接口不同的是,这个接口返回的是 HTML,上个接口返回的是 JSON,所以选择了重定向的方式,这样浏览器就可以获得到新的 st 了,具体代码如上,在 rewrite_by_lua_block 配置中使用 ngx.redirect() 方法来实现重定向。
同时这里再给你介绍下,如果业务校验不通过,想终止整个请求流程,可以通过 ngx.exit(状态码)来实现,这样就可以将对应状态码返回给前端了。但如果想要做得更友好些,当系统内部异常或者是后端服务器异常时,我们可以指定返回的内容。这就需要通过 error_page 指令来实现,意为出现不同的错误码,我们会转到不同的 location 去做处理。如果是 H5 请求,那就返回对应的错误提示页,如果是 JSON 请求,也可以返回自定义的 JSON 数据,如下所示:
#错误页location = /html_fail.html {default_type text/html;root /Users/Eleven/idea-practic/seckill-nginx/seckill-nginx/html;}#以"@"开头定义的location,是内部接口,外部无法访问location @json_fail {default_type application/json;return 200 '{"code":"200001","message":"nginx intercept!!!"}';}
上图就是配置了两个异常处理 location,其中 html_fail.html 的位置和内容如下:
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>抢购失败</title></head><body><div><p style="padding-left:240px;font-size:xx-large;color:red;">出现了一些问题,请再接再厉!!</p></div><script src="https://code.jquery.com/jquery-3.5.1.min.js"></script><script type="text/javascript"></script></body></html>
那么做完了上面那么多工作,现在我们就来验证下针对活动数据查询接口和结算页 H5 接口的顺序编排是否生效。
我们启动 Nginx,并且进入到商详页,点击立即抢购,正常流程是可以进入到结算页的,这里就不展示了,那如果我们不经过商详页,直接通过 URL 访问结算页 H5 接口(不带 st 或者带错误的 st),效果会如何呢?看下图:

出现了我们配置的错误提示页,同时日志中出现了 st 校验不通过的提示:

那么使用同样的机制,我们可以把剩下的接口功能给补上,因为做法一样,这里就不多说了。
到这,防刷的 Token 机制就介绍完了,这种机制可以有效防止黑产流量跳过中间接口,直接调用下单接口。通过该机制 +Nginx 有条件限流机制,可以有效拦截大部分场景下的刷子流量。但如果我们还想再严格一些,对于黑产不仅仅是想拦截过多的非法请求,而是想全部拦截,那有没有什么办法呢?有,下一篇我们继续介绍黑名单和风控机制。




