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 函数配合:

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 误杀和源站故障,还有几个常见场景:

  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,但会自动复制到全球边缘节点

亚马逊云科技官博原文: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...

相关推荐
赛博三把手1 天前
Claude Fable 5 API 调用完整指南:官方、AWS、OpenRouter 与中转平台对比(2026 最新)
网络·云计算·aws
Devlive 开源社区4 天前
一口气新增三家云——七牛云 Kodo、AWS S3、Cloudflare R2;并支持应用内自动检测与一键升级
云计算·aws
云服务器代理商5 天前
AWS Bedrock 指南:模型接入、Knowledge Base RAG、Agents 与计费方式
云计算·aws·亚马逊云服务器·aws bedrock·aws代理·亚马逊云代理商
望江东浪6 天前
Vector 选型与实战:vs OTel / Logstash / Fluentd 全维对比,及统一日志与指标管道的 AWS ECS 落地
云计算·aws
威联通网络存储11 天前
基于TS-h3087XU-RP的大型成套空分设备DCS历史趋势数据治理
aws
A小辣椒1 个月前
AWS Clould Support Engineer就职面试题
aws
亚林瓜子1 个月前
AWS WAF中如何放行某个触发了托管规则的接口
aws·waf
悠悠121381 个月前
AWS DevOps Agent 体验一周后,我决定把 oncall 手机调成静音了
云计算·aws·devops
yyuuuzz1 个月前
独立站运营的几个技术层面常见问题
大数据·运维·服务器·网络·数据库·aws
yyuuuzz1 个月前
游戏云服务器推荐的技术选择思路
大数据·运维·服务器·游戏·云计算·aws