WAF 误杀了正常请求怎么补数据?CloudFront + Lambda@Edge 双函数架构实战
被 WAF 拦了一批正常请求,body 没存下来,怎么办?最近看到亚马逊云科技官博的一个方案挺有意思------在 CDN 层用两个 Lambda@Edge 函数,一个存 body,一个记日志,全程不改源站代码。
亚马逊云科技官博今天(3/31)发了一篇实战文章:用 Amazon CloudFront 双 Lambda@Edge 架构,在不改源站代码的前提下,完整记录被拦截和出错的请求(含 headers 和 body),然后异步重放补数。
这思路挺巧的,拆解一下。
先说痛点
做过 CDN + WAF 架构的都遇到过这几个问题:
- WAF 误杀:安全规则有时候拦的是正常业务请求,事后想找回原始请求,只有日志里的 URL 和 status code,body 没了
- 源站临时挂了:返回 500/502 的请求,CDN 层面只能看到"失败了",具体请求内容不知道
- 改源站不现实:源站可能在第三方云、可能是供应商的系统、可能没代码权限
传统方案要么改源站代码加中间件,要么用 CloudFront 实时日志(但不含 body)。这次的方案在 CDN 层解决了所有问题。
架构核心:双 Lambda@Edge
整个方案用两个 Lambda@Edge 函数配合:
scss
请求 → 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 里读出来。
javascript
// 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 才触发记录逻辑。成功请求零开销。
javascript
// 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,不用管服务器。
yaml
# 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 读取后统一处理。
补数重放脚本
python
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 误杀和源站故障,还有几个常见场景:
- API 网关限流补偿:前端 CDN 层限流返回 429,记录被限的请求,低峰期重放
- 灰度发布回滚:新版本出 bug 导致 500,记录这段时间的请求,回滚后重放
- 跨云架构调试:源站在其他云上,出了问题想在亚马逊云科技侧看完整请求信息
用 OpenClaw 做运维自动化的话,可以写个 Skill 定期扫描 S3 里的失败请求日志,自动判断是否需要重放、通知值班人员、或者直接执行重放脚本。
几个注意点
- body 大小限制:Lambda@Edge 的 request body 默认上限 1MB(可调到 40KB 在 viewer-request,1MB 在 origin-request)。超大 body 需要截断
- 性能影响:origin-request 阶段给所有请求加了一次 header 操作,延迟增加约 1-5ms
- 安全考虑:存储了完整请求 body,可能包含敏感数据。S3 桶必须加密,设置生命周期策略自动清理
- 区域限制:Lambda@Edge 只能部署在 us-east-1,但会自动复制到全球边缘节点
亚马逊云科技官博原文:aws.amazon.com/cn/blogs/ch... Amazon CloudFront:aws.amazon.com/cn/cloudfro... AWS Lambda@Edge:docs.aws.amazon.com/AmazonCloud... OpenClaw:github.com/openclaw/op...