WAF 误杀了正常请求怎么补数据?CloudFront + Lambda@Edge 双函数架构实战

WAF 误杀了正常请求怎么补数据?CloudFront + Lambda@Edge 双函数架构实战

被 WAF 拦了一批正常请求,body 没存下来,怎么办?最近看到亚马逊云科技官博的一个方案挺有意思------在 CDN 层用两个 Lambda@Edge 函数,一个存 body,一个记日志,全程不改源站代码。

亚马逊云科技官博今天(3/31)发了一篇实战文章:用 Amazon CloudFront 双 Lambda@Edge 架构,在不改源站代码的前提下,完整记录被拦截和出错的请求(含 headers 和 body),然后异步重放补数。

这思路挺巧的,拆解一下。

先说痛点

做过 CDN + WAF 架构的都遇到过这几个问题:

  1. WAF 误杀:安全规则有时候拦的是正常业务请求,事后想找回原始请求,只有日志里的 URL 和 status code,body 没了
  2. 源站临时挂了:返回 500/502 的请求,CDN 层面只能看到"失败了",具体请求内容不知道
  3. 改源站不现实:源站可能在第三方云、可能是供应商的系统、可能没代码权限

传统方案要么改源站代码加中间件,要么用 CloudFront 实时日志(但不含 body)。这次的方案在 CDN 层解决了所有问题。

架构核心:双 Lambda@Edge

整个方案用两个 Lambda@Edge 函数配合:

复制代码

请求 → CloudFront → [WAF 检查] ↓ 通过 origin-request Lambda@Edge (把 request body 存进自定义 header) ↓ 源站处理 ↓ origin-response Lambda@Edge (检测 4xx/5xx → 记录完整请求到 CloudWatch Logs) ↓ CloudWatch Logs → Kinesis Data Firehose → S3

关键设计:

1. body 怎么传递

Lambda@Edge 在 origin-request 阶段能拿到 request body,但 origin-response 阶段拿不到。所以第一个函数把 body 塞进自定义 header X-Original-Body,第二个函数从 header 里读出来。

复制代码

// origin-request Lambda@Edge exports.handler = async (event) => { const request = event.Records[0].cf.request; if (request.body && request.body.data) { // 把 body 存进自定义 header request.headers['x-original-body'] = [{ key: 'X-Original-Body', value: request.body.data }]; } return request; };

2. 只记失败的

origin-response 阶段检查状态码,只有 4xx/5xx 才触发记录逻辑。成功请求零开销。

复制代码

// origin-response Lambda@Edge exports.handler = async (event) => { const response = event.Records[0].cf.response; const request = event.Records[0].cf.request; const status = parseInt(response.status); if (status >= 400) { const failedRequest = { timestamp: new Date().toISOString(), uri: request.uri, method: request.method, headers: request.headers, body: request.headers['x-original-body'] ? request.headers['x-original-body'][0].value : null, responseStatus: status }; // 写入 CloudWatch Logs console.log(JSON.stringify(failedRequest)); } return response; };

3. 日志怎么归档

Lambda@Edge 的日志自动进 Amazon CloudWatch Logs,加个 Subscription Filter 把日志投递到 Amazon Kinesis Data Firehose,再落到 Amazon S3。整条链路全 Serverless,不用管服务器。

复制代码

# CloudFormation 片段 Resources: LogSubscription: Type: AWS::Logs::SubscriptionFilter Properties: DestinationArn: !GetAtt DeliveryStream.Arn FilterPattern: '"failedRequest"' LogGroupName: !Ref LambdaLogGroup DeliveryStream: Type: AWS::KinesisFirehose::DeliveryStream Properties: S3DestinationConfiguration: BucketARN: !GetAtt FailedRequestsBucket.Arn BufferingHints: IntervalInSeconds: 60 SizeInMBs: 5

WAF 拦截的请求怎么办

被 AWS WAF 拦截的请求根本到不了 origin-request 阶段。但 WAF 自己有日志,包含 headers 和 body 前 8KB。同样通过 CloudWatch Logs → Kinesis → S3 归档。

两条路径最终汇聚到同一个 S3 桶,补数脚本从 S3 读取后统一处理。

补数重放脚本

复制代码

import json import boto3 import requests s3 = boto3.client('s3') def replay_failed_requests(bucket, prefix, target_url): """从 S3 读取失败请求日志并重放""" paginator = s3.get_paginator('list_objects_v2') for page in paginator.paginate(Bucket=bucket, Prefix=prefix): for obj in page.get('Contents', []): response = s3.get_object(Bucket=bucket, Key=obj['Key']) for line in response['Body'].read().decode().split('\n'): if not line.strip(): continue record = json.loads(line) # 重放请求 replay_response = requests.request( method=record['method'], url=f"{target_url}{record['uri']}", headers={k: v[0]['value'] for k, v in record['headers'].items() if k.lower() not in ('host', 'x-original-body')}, data=record.get('body') ) print(f"重放 {record['uri']}: {replay_response.status_code}")

成本分析

这方案的成本很可控:

组件 计费方式 估算(月 100 万请求,1% 失败率)
Lambda@Edge 按调用次数 + 执行时间 origin-request 处理所有请求,origin-response 只记失败的
CloudWatch Logs 按数据量 只有失败请求产生日志
Kinesis Data Firehose 按数据量 和日志量成正比
S3 存储 + 请求 日志文件通常很小

1% 失败率意味着每月只记录 1 万条请求,日志数据量可能就几十 MB。

实际用这个方案的场景

除了官博提到的 WAF 误杀和源站故障,还有几个常见场景:

  1. API 网关限流补偿:前端 CDN 层限流返回 429,记录被限的请求,低峰期重放
  2. 灰度发布回滚:新版本出 bug 导致 500,记录这段时间的请求,回滚后重放
  3. 跨云架构调试:源站在其他云上,出了问题想在亚马逊云科技侧看完整请求信息

用 OpenClaw 做运维自动化的话,可以写个 Skill 定期扫描 S3 里的失败请求日志,自动判断是否需要重放、通知值班人员、或者直接执行重放脚本。

几个注意点

  1. body 大小限制:Lambda@Edge 的 request body 默认上限 1MB(可调到 40KB 在 viewer-request,1MB 在 origin-request)。超大 body 需要截断
  2. 性能影响:origin-request 阶段给所有请求加了一次 header 操作,延迟增加约 1-5ms
  3. 安全考虑:存储了完整请求 body,可能包含敏感数据。S3 桶必须加密,设置生命周期策略自动清理
  4. 区域限制:Lambda@Edge 只能部署在 us-east-1,但会自动复制到全球边缘节点
相关推荐
海兰6 分钟前
【实战】HiMarket本地化部署指南
人工智能·ubuntu·架构·银行系统
高梦轩17 分钟前
MySQL 故障排查与优化
数据库·mysql
吴声子夜歌32 分钟前
Node.js——操作MySQL数据库
数据库·mysql·node.js
爱丽_33 分钟前
MySQL 锁等待与死锁进阶:怎么看等待、怎么降冲突(工程化套路)
数据库·mysql
心有—林夕43 分钟前
MySQL 误操作恢复完全指南
android·数据库·mysql
夕除1 小时前
Mysql--15
java·数据库·mysql
野生技术架构师1 小时前
掌握SQL窗口函数,轻松处理复杂数据分析
数据库·sql·数据分析
小程故事多_801 小时前
自然语言智能体控制框架,重塑AI Agent的协作与执行范式
人工智能·架构·aigc·ai编程·harness
会飞的大可1 小时前
NoSQL:从原理到实践的全景指南
数据库·nosql
2501_933329551 小时前
技术深度拆解:Infoseek舆情系统的全链路架构与核心实现
开发语言·人工智能·分布式·架构