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

张开发
2026/4/18 19:44:39 15 分钟阅读

分享文章

WAF 误杀了正常请求怎么补数据?CloudFront + Lambda@Edge 双函数架构实战
WAF 误杀了正常请求怎么补数据CloudFront LambdaEdge 双函数架构实战被 WAF 拦了一批正常请求body 没存下来怎么办最近看到亚马逊云科技官博的一个方案挺有意思——在 CDN 层用两个 LambdaEdge 函数一个存 body一个记日志全程不改源站代码。亚马逊云科技官博今天3/31发了一篇实战文章用 Amazon CloudFront 双 LambdaEdge 架构在不改源站代码的前提下完整记录被拦截和出错的请求含 headers 和 body然后异步重放补数。这思路挺巧的拆解一下。先说痛点做过 CDN WAF 架构的都遇到过这几个问题WAF 误杀安全规则有时候拦的是正常业务请求事后想找回原始请求只有日志里的 URL 和 status codebody 没了源站临时挂了返回 500/502 的请求CDN 层面只能看到失败了具体请求内容不知道改源站不现实源站可能在第三方云、可能是供应商的系统、可能没代码权限传统方案要么改源站代码加中间件要么用 CloudFront 实时日志但不含 body。这次的方案在 CDN 层解决了所有问题。架构核心双 LambdaEdge整个方案用两个 LambdaEdge 函数配合请求 → CloudFront → [WAF 检查] ↓ 通过 origin-request LambdaEdge (把 request body 存进自定义 header) ↓ 源站处理 ↓ origin-response LambdaEdge (检测 4xx/5xx → 记录完整请求到 CloudWatch Logs) ↓ CloudWatch Logs → Kinesis Data Firehose → S3关键设计1. body 怎么传递LambdaEdge 在 origin-request 阶段能拿到 request body但 origin-response 阶段拿不到。所以第一个函数把 body 塞进自定义 headerX-Original-Body第二个函数从 header 里读出来。// origin-request LambdaEdge 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 LambdaEdge 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. 日志怎么归档LambdaEdge 的日志自动进 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: 5WAF 拦截的请求怎么办被 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(Bucketbucket, Prefixprefix): for obj in page.get(Contents, []): response s3.get_object(Bucketbucket, Keyobj[Key]) for line in response[Body].read().decode().split(\n): if not line.strip(): continue record json.loads(line) # 重放请求 replay_response requests.request( methodrecord[method], urlf{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)}, datarecord.get(body) ) print(f重放 {record[uri]}: {replay_response.status_code})成本分析这方案的成本很可控组件计费方式估算月 100 万请求1% 失败率LambdaEdge按调用次数 执行时间origin-request 处理所有请求origin-response 只记失败的CloudWatch Logs按数据量只有失败请求产生日志Kinesis Data Firehose按数据量和日志量成正比S3存储 请求日志文件通常很小1% 失败率意味着每月只记录 1 万条请求日志数据量可能就几十 MB。实际用这个方案的场景除了官博提到的 WAF 误杀和源站故障还有几个常见场景API 网关限流补偿前端 CDN 层限流返回 429记录被限的请求低峰期重放灰度发布回滚新版本出 bug 导致 500记录这段时间的请求回滚后重放跨云架构调试源站在其他云上出了问题想在亚马逊云科技侧看完整请求信息用 OpenClaw 做运维自动化的话可以写个 Skill 定期扫描 S3 里的失败请求日志自动判断是否需要重放、通知值班人员、或者直接执行重放脚本。几个注意点body 大小限制LambdaEdge 的 request body 默认上限 1MB可调到 40KB 在 viewer-request1MB 在 origin-request。超大 body 需要截断性能影响origin-request 阶段给所有请求加了一次 header 操作延迟增加约 1-5ms安全考虑存储了完整请求 body可能包含敏感数据。S3 桶必须加密设置生命周期策略自动清理区域限制LambdaEdge 只能部署在 us-east-1但会自动复制到全球边缘节点

更多文章