使用openresty打造接口校验服务
Posted 拖地先生
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用openresty打造接口校验服务相关的知识,希望对你有一定的参考价值。
正文共: 5665字 3图,预计阅读时间: 15分钟
温旭峰好文:
为什么要做?
线上出现了刷接口行为,产生了数倍的非正常请求。下图是上线了接口校验服务前后某接口的变化:
我们先了解以下几个问题:
为什么会有人刷接口?
牟利
恶意攻击竞争对手
压测
脚本小子
怎么定义刷?
频率高
次数多
难识别
当我们遭遇了刷接口时我们该怎么做?
初期我们采用了在负载均衡侧直接禁用ip的方式,有一定的效果;但随后刷接口的手段升级,采用ip池的方式后,禁用ip的方法就显得捉襟见肘了;这种时候有几种常见的处理方式:
拒绝请求
接口频率限制、动态限制ip
人机验证
请求信息校验
为什么要处理刷接口?
有效的减轻后端服务的压力
可以提升整体服务的可用性
前端调用接口的安全问题
前端web调用接口与app调用的接口共用一套,但是针对于前端web的调用,没有加入sign的校验,也没有任何其他校验性的逻辑,这样就导致拿到接口后,可以随意传递参数,随意调用,缺乏安全性。
基于上述两点,我们需要上线一套基于现有逻辑的接口校验服务;来结束前端接口调用的裸奔状态,并期望可以把刷接口的请求屏蔽,减少不必要的计算,也避免信息外泄。
如何选?
我们在选择安全方案上需要考虑便捷性、可操作性的问题,如果是安全性比较高,没有可操作性的方案,那也只能选择放弃;上面我们说了几种常见的处理方式,我们结合项目实际逐条分析利弊。
拒绝请求
拒绝所有的接口请求,这是最有效、直接、简单、粗暴的处理方式。但是我们的被刷的是入口功能,不能采取这样的方式处理。
接口频率限制、动态限制IP
客户端请求的时候 , 把ip记录下来,每次访问这个ip访问次数加一,如果超过指定次数,把这个ip拉黑。这种方式存在以下几个问题:
次数如何设定;这是一个需要不断尝试来确定的值,且还需要根据业务流量的变化来跟着变化
分布式服务中复杂度高;单体服务可以采用本机缓存,如果是分布式服务的话,就需要采用redis或memcache这类分布式缓存组件来支撑,以保证限制的准确性及有效性
入口接口比较特殊,可能涉及到推广业务,所以这个频率并不好控制,也不能根绝过往经验拍脑袋决定;另外就是再启动一套缓存组件或限流的中间件来支撑,有一定的成本;我们更倾向于将问题的解决前置,而不是后置到服务中。
人机验证
人机验证的方式有很多:如短信验证码、图片验证码、12306的验证、Google reCAPTCHA
这类验证可以挡掉大部分攻击流量,也是较为常见的处理方式,也是登录注册常见的组件功能。由于历史原因,我们的前端项目并不是使用统一的组件、统一的类库来处理接口请求,无法做到更改一处,随着项目更新将人机验证的代码上线,就导致我们需要更改大量的项目才能配合我们完成这项工作,时间成本及风险比较高。
另外一个原因:一般情况下,客户端很少情况触发人机验证规则。如过每次都做验证的话,会影像到用户的使用体验,尤其是新用户的使用。
请求信息校验
这种方式是指对请求信息中的header信息或body信息做验证,合法的才允许访问到后端服务,不合法的就直接返回错误信息。常见的有User-Agent的验证、referer的验证、签名sign字段的验证、cookie的验证等
这种方式对调用的规范性有一定要求;如果部分业务未使用通用的规范,那就要兼容或舍弃这部分业务请求。这种方式仅服务端即可处理,不需要客户端或前端项目发布配置。
怎么做?
我们采用请求信息校验的方式进行拦截非法请求,将问题的解决前置到nginx服务中,将非法请求拦截在php-fpm之前。
openresty介绍
开发nginx的模块需要C语言,同时需要熟悉nginx源码,成本与门槛比较高,而openresty 把LuaJIT VM嵌入到nginx中,使得我们可以直接通过lua脚本在nginx上进行编程,同时openresty提供了大量的类库。所以我们采用了nginx+lua的方式进行接口校验服务的开发,用openresty替代nginx。
nginx采用master-worker模型,一个master进程管理多个worker进程,worker真正负责对客户端的请求处理。master仅负责一些全局初始化、对worker进行管理。在openresty中,每个worker中有一个Lua VM,当一个请求被分配到worker时,Lua创建一个coroutine来负责处理。
nginx把一个请求分成了很多阶段,第三方模块可以根据自己的行为,挂载到不同阶段进行处理以达到目的;以下是各个阶段的解释及使用范围:
阶段指令 | 使用范围 | 解释 |
---|---|---|
initbylua*, initworkerby_lua* | http | 初始化全局配置,预加载lua模块 |
setbylua* | server, server if, location, location if | 设置nginx变量,此处是阻塞的,lua代码要做到非常快 |
rewritebylua* | http, server, location, location if | rewrite处理阶段;可以实现复杂的转发、重定向逻辑 |
accessbylua* | http, server, location, location if | 请求访问处理阶段;用于访问控制 |
contentbylua* | location, location if | 内容处理阶段;接收请求处理并输出响应 |
headerfilterby_lua* | http, server, location, location if | 设置header与cookie |
bodyfilterby_lua* | http, server, location, location if | 对响应数据进行过滤,比如截断、替换 |
logbylua* | http, server, location | log处理阶段;比如记录访问量、统计平均影响时间 |
处理流程
前端的核心思路是:用户请求.html文件访问页面时,文件内容中动态写入拦截XHR逻辑、设置标记并禁用缓存。在accessbylua_file中加入验证逻辑。
同时,还需要在nginx配置如下:
nginx.conf 主要是加载lua类库,并开启lua代码缓存
user nobody nobody;
worker_processes 4;
# ....
http {
# ....
lua_package_path "/path/to/lua/lib/?.lua;;";
lua_code_cache on;
# ....
}xxx.conf 业务配置,所有流程中的处理配置都在此文件中
server {
# ... listen index root 配置
location ~ .*\.php?$ {
access_by_lua_file /path/to/lua/access.lua;
# ... fastcgi 配置
}
# ...其他配置
# 接口转发
location ~ / {
rewrite ^/(.*) /$1/index.php last;
}
# 前端页面所有html页面统一处理
location ~ / {
header_filter_by_lua_file /path/to/lua/mark.lua;
body_filter_by_lua_file /path/to/lua/script.lua;
}
# ...其他配置
}
遇到了哪些问题?
1. 错误: attempt to set status 403
错误原文:attempt to set status 403 via ngx.exit after sending out the response status 200
ngx.say
会默认输出200的状态码
使用 ngx.say
之后如果整个流程中还有其他状态码的输出,就会输出这个错误;所以在调试阶段不建议使用ngx.say
,而是使用ngx.log
来打印日志调试代码
2. ngx.exit与ngx.eof区分
ngx.exit:当传入200(ngx.HTTPOK)或其他http状态码时,会中断当前请求,并将传入的状态码返回nginx,当传入0(ngx.OK)时,ngx.exit会中断当前执行的阶段,进而执行后续的阶段;但是不能屏蔽headerfilter_lua*的逻辑;推荐ngx.exit与return一起使用,目的在于增强请求被终止的语义
ngx.eof:只是结束响应流的输出,中断http链接,后面的代码逻辑依然会在服务端继续执行,ngx.eof有返回值,而ngx.exit没有
3. 前端页面地址使用不规范的问题
$request_uri 是客户端发送来的原生请求uri,包括参数,不可以进行修改
$uri 则是不包含任何参数,反映任何内部重定向或index模块所做的更改
4. response body被截断的问题
因为我们需要使用body_filter_by_lua
向html文件的<head>标签内动态加入安全逻辑,这样的话就更改了返回包内容的长度,导致response header中的content-length变短,导致页面内容无法完全加载。所以我们需要更新content-length为正确的值,有两个方法:
手动计算content-length,然后重新设置
忽略content-length,使用流方式处理,这种情况下response header会返回
Transfer-Encoding:chunked
;忽略的方式在openresty可使用ngx.header.content_length = nil
来实现
通常情况下,更建议使用成本更小的第二种方式处理被截断的问题
5. 浏览器缓存html文件
我们更新了html,让其加载拦截XHR请求;但是通常情况下,浏览器尤其是微信中会缓存html文件;在访问html文件时会传递if-modified-since
的请求头,如果服务端判断到没有更新,则会返回304,不会重新加载html文件内容;这时就会导致接口请求异常。
在http请求与响应中;我们可以通过Cache-Control
指令来实现缓存机制。关于可缓存性有以下几个取值:
public 可以被任何对象(请求客户端、代理服务器等)缓存
private 只能被单个用户缓存,代理服务器不能缓存
no-cache 在发布缓存副本前,强制要求缓存把请求提交给原始服务器进行验证,即协商缓存验证
no-store 不使用任何缓存,即不应存储有关客户端请求或服务器响应的任何内容
在服务端中设置response header中的cache-control;设置为no-cache或no-store,告诉客户端不要缓存html文件,可以解决此类问题,实现方式为 ngx.header.cache_control = "no-store"
6. 小程序的处理
需要注意小程序比较特殊,没有时机设置标记,也无法操作cookie。无法通过user-agent来拆出小程序的请求,因为小程序的user-agent也是不规范的,每个版本都可能不一样。
7. 内部调用的处理
其他服务也可能会用到线上提供的api接口,但是服务中调用不规范统一处理就不切实际,需要先改造之后才可以进行统一处理,不规范主要表现为:
不区分内网与公网;服务中有使用内网SLB调用api的,也有通过公网SLB调用api的;由于服务会启动弹性伸缩,这样就导致无法通过调用接口的来源ip来判定内部调用
黑魔法调用参数;通过一个特定的参数来标记内部服务调用需要的特殊处理逻辑
调用没有统一处理;往往都是用到的时候自己写一个,没有一个统一的类库来处理,就导致处理时要全局搜索,影响面广,工作量大
有哪些结论?
1. 规范的重要性
主要表现在以下几个方面:
前端项目的规范
访问路径混乱、不统一、随意
通用基础库没有统一管理,一个项目一套东西
上面两个方面更多的是历史遗留问题造成的,统一规范访问路径,提取通用基础库,不用每个项目都做重复性的工作。
接口调用的规范
上面内部调用处理详细说明了存在的问题以及处理方案:对于通用性的功能,都采用抽象出统一处理对象来处理,不建议过程化的处理,这样会对后续的抽象带来非常大的开发及测试工作量。
api使用的规范
由于使用的php框架是yaf,所以在调用接口时首字母大小写都是可以执行,这就导致采用什么方式的都有,没有一个明确的规范。给白名单处理时带来不必要的麻烦;在添加白名单时需要考虑到各种各样的情况,甚至穷举。
规范为:controller与module都使用小写,action使用首字母小写,其余的与action function name一致;如:
2. 安全工作
安全工作是一项长期的和攻击者斗智斗勇的工作。没有一劳永逸的解决方案,不断交锋,不断成长
拖地先生,从事互联网技术工作,在这里每周两篇文章,聊聊日常的实践和心得。往期推荐:
如果对你有帮助,让大家也看看呗~
以上是关于使用openresty打造接口校验服务的主要内容,如果未能解决你的问题,请参考以下文章
极客Live | 直播预告:OpenResty 架构解读与实战分享