CloudFront 作为内容分发网络,在边缘站点上提供了大量缓存和转发能力。但很多定制逻辑无法靠配置实现,比如按设备类型改写 URL、统一注入源站认证头、给所有响应添加安全头。Lambda@Edge 就是解决这类问题的计算层,它允许在 CloudFront 处理请求和响应的过程中执行自定义代码。需要特别注意的是,Lambda@Edge 并不是一个笼统的入口,而是区分了四个触发器阶段:查看器请求、源站请求、源站响应、查看器响应。每个阶段能在不同时间点接触不同的数据,选错阶段可能带来缓存命中率下降、响应头丢失或函数执行成本增加。

一、四个触发阶段分别发生在请求链路的哪个位置
要理解 Lambda@Edge 的触发器阶段,先要搞清楚一个请求从用户浏览器到源站再返回的完整链路。用户发起请求后,CloudFront 边缘站点首先收到该请求,这时会触发查看器请求阶段。随后 CloudFront 会根据缓存键查找是否已有可用缓存。如果缓存命中,就直接进入查看器响应阶段;如果缓存未命中,CloudFront 会向源站发起回源请求,在发出回源请求前触发源站请求阶段。
源站接收到请求并返回响应后,CloudFront 边缘站点在把响应写进缓存之前触发源站响应阶段。接着 CloudFront 将响应缓存下来,最后在把响应返回给用户浏览器前触发查看器响应阶段。这四个阶段的执行顺序是固定的,但触发条件不同:只要用户请求到达边缘站点,查看器请求和查看器响应就一定会执行;而源站请求和源站响应只有在缓存未命中、需要回源时才会执行。
可以用一个简化的列表来梳理每个阶段的触发时机与可操作对象:
- Viewer Request:用户请求到达 CloudFront 后、缓存查找之前触发。可操作用户请求头、URI、查询字符串等。
- Origin Request:缓存未命中、CloudFront 准备向源站转发请求前触发。可操作回源请求头、URI、请求体策略等。
- Origin Response:源站返回响应后、CloudFront 缓存响应之前触发。可操作源站响应头、状态码、响应体替换等。
- Viewer Response:CloudFront 准备将响应返回给用户前触发。可操作最终返回给用户的响应头、状态码等,但无法修改已被缓存的响应体内容。
理解这个链路后,就能更准确地判断自己的业务逻辑应该放在哪个阶段。一个常见误区是把所有逻辑都塞进查看器请求阶段,因为它在每个请求上都会执行,即使缓存命中也不例外,这往往会增加延迟和成本。
二、各阶段典型场景与代码示例
Viewer Request:入口处的轻量判断与重写
查看器请求阶段适合做与用户直接相关的轻量级处理,例如移动端重定向、A/B 测试流量分配、URL 规范化、根据 Cookie 做访问控制。由于这个阶段发生在缓存查找之前,执行的任何逻辑都会影响所有请求,包括缓存命中的请求,因此函数必须保持非常轻量,避免引入明显的延迟。
下面这段示例代码演示了如何把根路径和 /index 请求重写为 /index.html,避免源站返回 403 或不完整的目录响应。函数接收 CloudFront 事件,取出 request 对象,修改 URI 后直接返回。
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const uri = request.uri;
if (uri === '/' || uri === '/index') {
request.uri = '/index.html';
}
return request;
};
这个阶段还能根据 User-Agent 判断设备类型,把移动端用户重定向到 /mobile 子目录。需要注意的是,重定向通常需要返回一个响应对象而不是修改请求对象,否则请求会继续向源站转发。在查看器请求阶段返回重定向响应是允许的,且不会占用回源资源。
Origin Request:把源站相关逻辑收敛到回源前
源站请求阶段只在缓存未命中时执行,因此它是处理源站相关逻辑的理想位置。比如给回源请求添加固定的认证头、修改回源路径以匹配源站目录结构、或者从边缘端剥离用户不需要的 Cookie,降低回源请求体积。
以下示例展示了如何只在 /api/ 路径下添加一个内部认证头。这样源站可以只信任带有该认证头的请求,避免用户直接绕过 CloudFront 访问源站时伪造请求头。
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
const uri = request.uri;
if (uri.startsWith('/api/')) {
headers['x-origin-auth'] = [{ key: 'X-Origin-Auth', value: 'token-from-edge' }];
}
return request;
};
由于源站请求阶段只在回源时触发,比查看器请求阶段更适合执行涉及源站配置的逻辑。把认证、路径重写等操作收敛到这里,可以避免对缓存命中请求产生不必要的开销。同时要注意,这个阶段的执行超时时间比查看器阶段更长,可以承担稍重一些的处理,但仍不建议在函数中同步调用外部服务。
Origin Response:缓存写入前统一处理响应头
源站响应阶段发生在源站返回响应之后、CloudFront 写缓存之前。这里非常适合添加统一的响应安全头、设置 CORS 头、改写状态码或处理源站返回的错误页面。由于响应来自源站且尚未进入缓存,此时修改的效果会被 CloudFront 缓存下来,后续命中缓存的响应也会带上这些修改。
下面这段代码给所有源站响应添加了 HSTS 和 X-Frame-Options 头。这些头一旦被缓存,就不需要每个请求都执行逻辑,从而降低查看器响应阶段的压力。
exports.handler = async (event) => {
const response = event.Records[0].cf.response;
const headers = response.headers;
headers['strict-transport-security'] = [
{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubdomains' }
];
headers['x-frame-options'] = [
{ key: 'X-Frame-Options', value: 'DENY' }
];
return response;
};
如果把安全头添加逻辑放在查看器响应阶段,那么每次响应返回给用户时都要执行函数,即使响应已经被缓存过。而源站响应阶段则只在回源时执行一次,之后直接从缓存读取带着安全头的响应,成本和延迟都更低。这是阶段选择对性能影响的典型例子。
Viewer Response:最终交付前的个性化清理
查看器响应阶段发生在 CloudFront 把响应返回给用户之前的最后一刻。它适合处理那些不适合被缓存的、与具体用户相关的响应头或清理敏感信息。例如移除源站暴露的 Server、X-Powered-By 等内部技术栈信息,或者根据用户 Cookie 动态设置额外的个性化响应头。
下面的示例演示了如何删除源站返回的一些不必要响应头。这个操作必须在最终响应离开边缘节点前完成,而且这些删除动作不会被缓存,确保每次响应都能执行清理。
exports.handler = async (event) => {
const response = event.Records[0].cf.response;
const headers = response.headers;
delete headers['server'];
delete headers['x-powered-by'];
delete headers['x-aspnet-version'];
return response;
};
需要注意的是,查看器响应阶段无法修改已经被 CloudFront 缓存的响应体,只能操作响应头和状态码。如果需要修改响应体,可以考虑使用源站响应阶段或配合 Lambda 函数生成动态响应。此外,查看器响应阶段的超时时间也较短,只适合做轻量清理和定制。
三、部署与测试 Lambda@Edge 函数的注意事项
Lambda@Edge 函数有一个特殊的地域限制:函数必须创建在美国东部(弗吉尼亚北部)的 us-east-1 区域。之后 CloudFront 会自动把函数复制到全球边缘站点执行。如果你在其它区域创建 Lambda 函数,将无法直接关联到 CloudFront 分配,需要先在 us-east-1 区域部署并发布版本。
部署时通常先编写普通 Lambda 函数,然后通过 AWS CLI 或控制台在 us-east-1 创建函数。下面是一个使用 AWS CLI 创建函数的示例,注意使用 nodejs20.x 运行时,并指定正确的 IAM 角色。
aws lambda create-function \ --function-name edge-security-headers \ --runtime nodejs20.x \ --role arn:aws:iam::123456789012:role/service-role/edge-lambda-role \ --handler index.handler \ --zip-file fileb://function.zip \ --region us-east-1
创建完成后,Lambda@Edge 要求必须先发布一个版本,而不能直接使用 $LATEST。版本发布后才能在 CloudFront 的缓存行为中配置关联。下面是发布版本的命令:
aws lambda publish-version \ --function-name edge-security-headers \ --description "add security headers" \ --region us-east-1
随后在 CloudFront 分配的行为设置中,选择对应的 Lambda 函数 ARN 并指定触发器阶段即可。关联完成后,函数会被复制到边缘站点,这可能需要几分钟时间。测试时可以在 Lambda 控制台选择 CloudFront 测试事件模板,模拟不同阶段的输入结构。日志会分散在 CloudFront 边缘节点对应的区域,不一定集中在 us-east-1,需要在 CloudWatch 中查看对应区域的日志组。
四、性能影响、硬性限制与最佳实践
Lambda@Edge 在执行时会增加请求的延迟,尤其是查看器阶段,因为每次请求都会触发函数。因此要尽量避免在查看器请求和查看器响应阶段做复杂计算。查看器阶段的超时时间是 5 秒,源站阶段的超时时间是 30 秒,但实际业务中如果函数运行超过几百毫秒,用户就能明显感受到延迟。
Lambda@Edge 还有一些硬性限制需要提前了解。查看器阶段函数的部署包大小限制为 1MB,源站阶段为 50MB。函数内存范围为 128MB 到 3008MB。环境变量、VPC 访问、X-Ray 跟踪等功能在 Lambda@Edge 中不可用,函数也不能使用弹性文件系统。此外,每个函数只能与一个 CloudFront 发行版关联特定数量的触发器,具体配额可以在 AWS 文档中查看。
从性能角度看,合理的阶段选择比函数本身的优化更重要。能放进源站阶段的逻辑就不要放在查看器阶段,因为源站阶段仅在缓存未命中时执行。这样既能减少函数调用次数,也能降低缓存命中请求的延迟。对于响应头的添加,优先选择源站响应阶段,让修改后的响应直接被缓存。
在函数实现上,建议保持逻辑简单、避免同步调用外部 API,因为外部调用会显著增加延迟并可能超时。如果确实需要访问外部数据,可以考虑在部署前预取并打包进函数,或者使用 CloudFront 的缓存策略减少回源。同时要为函数编写错误处理逻辑,避免因为抛异常导致请求失败。Lambda@Edge 默认对函数异常有保护行为,但主动处理错误能让响应更加可控。
总结来说,四种触发器阶段为 CloudFront 提供了灵活的边缘计算能力,但每种阶段对应不同的数据可见性和性能成本。理解请求链路和缓存行为之后,把逻辑放到正确的阶段,是使用 Lambda@Edge 最重要的决策。选对阶段,很多看似复杂的需求只需要十几行代码就能在边缘稳定运行。
AWS CloudFrontLambda@Edge触发器阶段修改时间:2026-09-20 11:10:59