背景
在今年 7 月的用户增长活动中,由于大规模流量访问活动落地页,不仅使活动 api 响应缓慢,点单小程序整体功能也受到影响。活动结束后,为了减少活动 api 对小程序点单的影响,toC 端业务组开展小程序稳定性专项治理。一期工作如下:
- 新开一个域,命名为 h5api,专门用来承接活动落地页的流量。
- 活动落地页(h5 网页)实现前后端域名分离。
- 活动落地页开启 CDN 加速并回源到 OSS。
故障时的表现
前端的表现:
用户在活动落地页提交答案时,控制台有如下报错:
线上环境的 CORS 预检请求的响应头没有任何字段,导致浏览器不发送随后的正式请求,控制台出现跨域报错。
网关的表现:
答题接口的流量大概有 99% 被 waf 拦截。
后端服务的表现:
答题接口几乎没有流量。
总结与思考
恢复故障耗时 90 分钟,从故障复盘会得知:
- 网络安全人员:不知道浏览器发送正式的跨域请求前会先发送预检请求。
- 前端开发人员:不知道 waf 的一条拦截规则是,拦截未携带 authorization 标头的答题接口(/xxx/xxx/fcfs)。预检请求的请求方法是 OPTIONS,它未携带 authorization 请求头,waf 将其拦截,浏览器收到的响应头不包含任何字段,则不发起正式请求。
本项目是跨团队协作项目,涉及的职能有:前端、后端、测试、运维和网络安全,回顾从项目启动到结束全过程:
- x 月 x 日开项目立项会,会议文档中试图罗列所有的改动点,包含前端代码层面的改动、后端服务改动、ingress 配置改动和上线切换。
- 6 天后前端开技术方案评审会议,技术方案文档中试图罗列域名解析如何改动,ingress 如何改动。
这两次会议的问题是,会议主持人试图猜测协作方需要做哪些工作,以自己的猜测安排协作方的工作,实际上没有做到把控会议文档中的全部内容,这导致一些协作方沦为执行者的角色,同时在项目过程中没人察觉文档中的遗漏项。
从本项目和引发的故障中总结经验,一个正向的项目方案要经历的步骤是:
- 项目立项:说明项目背景和目标、拉齐协作方、确认上下游关系和各协作方负责人。
- 方案评审:默认每一个职能都需要写技术方案文档,如果评估后不需要单独的技术方案文档则说明原因。每个职能确认技术方案时,邀请下游协作方入会,向下游说明自己为它提供的输入。从网络分层模型可知,本项目的上下游关系:前端 -> 网络安全 -> 运维 -> 后端。
故障后改进:
- 活动开始前组织活动会议群,拉相关方入会,相关方作为活动用户体验活动,遇到问题时在群里反馈,确认活动数据无异常后散会。该措施用来解决出现故障时拉不齐人员的问题。
- 优化项目立项流程,前文已提到。
CORS 的知识点
CORS(Cross-Origin Resource Sharing,跨源资源共享)是一种安全机制,它允许或限制网页从另一个源访问资源。当一个网页尝试从与其不同的源请求资源时,就会涉及到CORS。两个源相同则说明它们的域名、协议和端口都分别相同,否则不同源。前文提到活动落地页实现前后端域名分离,指的是实现前端资源和后端服务不同源。
简单请求和复杂请求
请求同时满足以下条件,则被认为是简单请求:
- 使用GET、HEAD或POST方法。
- HTTP头部信息不超出以下几种字段:
Accept
、Accept-Language
、Content-Language
、Content-Type
(限于三个值:application/x-www-form-urlencoded
、multipart/form-data
、text/plain
)。 - 请求中的任何XMLHttpRequestUpload对象均没有注册任何事件监听器;XMLHttpRequest对象没有使用
withCredentials
属性。 - 请求中没有使用 ReadableStream 对象。
不是简单请求则是复杂请求。
简单请求发送和响应流程
浏览器直接发送简单请求到服务器,如果服务器返回的响应头 Access-Control-Allow-Origin 能匹配当前网页所在的源,那么将响应的内容提供给前端 JS 代码,否则阻止前端代码访问响应内容。
复杂请求发送和响应流程
预检请求
浏览器发送预检请求
如果请求不是简单请求,浏览器会先发送一个预检请求(OPTIONS方法),询问目标服务器是否允许跨域请求。预检请求头需要包含下列字段:
- Access-Control-Request-Method 头部:说明正式请求将使用的方法
- Access-Control-Request-Headers 头部:列出正式请求将要发送的自定义头部
- Origin 头部:发送请求的源。
服务器响应预检请求
服务器收到预检请求后判断是否允许接下来的正式请求,如果是,则在响应中包含下列字段:
- Access-Control-Allow-Origin 头部:允许的正式请求的源
- Access-Control-Allow-Methods 头部:允许的正式请求的方法
- Access-Control-Allow-Headers 头部:允许的正式请求的头部
正式请求
浏览器发送正式请求
浏览器收到预检请求的响应后,检查服务器是否允许跨域访问,是则继续发送正式请求。
服务器响应正式请求
服务器处理正式请求,并在响应中包含 Access-Control-Allow-Origin。
浏览器处理响应
浏览器检查响应头 Access-Control-Allow-Origin 是否能匹配当前网页的源,是则将响应的内容提供给前端 JS 代码,否则阻止前端代码访问响应内容。