CloudFront Lambda@Edge Set-Cookie头可见但浏览器未写入Cookie问题排查
故障根因
问题由三类问题共同导致:
- Cookie属性配置缺失:现有Set-Cookie头未设置
Path=/属性,默认Cookie生效范围为首次请求的子路径,其他路径发起的请求不会携带该Cookie;若使用SameSite=None配置,除了必须搭配Secure属性外,跨域场景下若CORS配置未开启凭证允许,浏览器会直接拒绝写入Cookie。 - 代码逻辑缺陷:
- Viewer Request阶段仅从querystring提取jwt,未实现Cookie读取逻辑,querystring无jwt时直接返回401;
- querystring参数解析用
split("jwt=")的写法存在兼容问题,当jwt不是querystring最后一个参数时,提取到的token会携带后续参数内容,直接导致验签失败; - base64url解码逻辑未处理JWT使用的非标准base64字符(
-/_)及补位符,部分token会解码失败; - Viewer Response阶段直接覆盖
set-cookie头数组,会清空源站返回的其他Cookie,且未判断token是否存在,querystring无jwt时会写入token=undefined的无效Cookie;响应阶段逻辑异常时直接返回401,会中断正常响应。
- CloudFront配置遗漏:
- 若缓存策略未将Cookie头、jwt query参数纳入转发白名单,CloudFront会截断对应内容,Lambda@Edge读不到Cookie、客户端收不到Set-Cookie头;
- 若分发允许HTTP访问,Secure标记的Cookie不会被浏览器存储;
- 跨域场景下若响应头未正确配置CORS凭证相关参数,浏览器会拦截Cookie写入。
分步修复方案
1. CloudFront必要配置调整
- 调整绑定对应分发行为的缓存策略:将
Cookie设置为全量转发或至少转发名为token的Cookie;Querystring中将jwt加入转发白名单,避免参数被CloudFront缓存层截断。 - 调整响应头策略:不要在托管/自定义响应头策略中覆盖
Set-Cookie头,确保Lambda@Edge写入的响应头能透传给客户端;跨域场景下需将Access-Control-Allow-Credentials设为true,且Access-Control-Allow-Origin不能配置为通配符*,需填写明确的前端站点域名。 - 调整查看器协议策略:选择「重定向HTTP到HTTPS」,兼容Secure属性Cookie的存储要求。
2. Viewer Request 函数修复
修复参数解析、Cookie读取、base64解码逻辑,修复后代码如下:
const crypto = require("crypto"); const response401 = { statusCode: 401, statusDescription: 'Unauthorized', headers: { 'content-type': [{ key: 'Content-Type', value: 'text/plain' }] }, body: 'Unauthorized' }; // 简易Cookie解析工具 function parseCookies(cookieHeader) { const cookies = {}; if (!cookieHeader) return cookies; cookieHeader.split(';').forEach(cookie => { const [name, ...rest] = cookie.trim().split('='); cookies[name.trim()] = decodeURIComponent(rest.join('=')); }); return cookies; } exports.handler = async (event, context) => { const request = event.Records[0].cf.request; const key = "替换为你的实际SECRET KEY"; let token = null; // 优先从querystring取jwt,兼容任意参数位置 const queryParams = new URLSearchParams(request.querystring); if (queryParams.has('jwt')) { token = queryParams.get('jwt'); } else { // querystring无jwt时,从Cookie头读取token const cookieHeader = request.headers.cookie ? request.headers.cookie[0].value : ''; const cookies = parseCookies(cookieHeader); token = cookies.token || null; } if(!token){ console.log("Missing authorization token."); return response401; } try { jwt_decode(token,key) } catch (error) { console.log("Token validate failed:", error); return response401; } return request; }; function jwt_decode(token, key, noVerify, algorithm) { if (!token) throw new Error("No token supplied"); var segments = token.split("."); if (segments.length !== 3) throw new Error("Not enough or too many segments"); var headerSeg = segments[0]; var payloadSeg = segments[1]; var signatureSeg = segments[2]; var header = JSON.parse(_base64urlDecode(headerSeg)); var payload = JSON.parse(_base64urlDecode(payloadSeg)); if (!noVerify) { var signingMethod = "sha256"; var signingType = "hmac"; var signingInput = [headerSeg, payloadSeg].join("."); if (!_verify(signingInput, key, signingMethod, signingType, signatureSeg)) { throw new Error("Signature verification failed"); } if (payload.nbf && Date.now() < payload.nbf * 1000) { throw new Error("Token not yet active"); } if (payload.exp && Date.now() > payload.exp * 1000) { throw new Error("Token expired"); } } return payload; } function _verify(input, key, method, type, signature) { if (type === "hmac") { return signature === _sign(input, key, method); } else { throw new Error("Algorithm type not recognized"); } } function _sign(input, key, method) { return crypto.createHmac(method, key).update(input).digest("base64url"); } // 修复base64url解码逻辑,兼容JWT编码格式 function _base64urlDecode(str) { let base64 = str.replace(/-/g, '+').replace(/_/g, '/'); const pad = base64.length % 4; if (pad) { if (pad === 1) throw new Error('Invalid base64url string'); base64 += '='.repeat(4 - pad); } return Buffer.from(base64, "base64").toString("utf-8"); }
3. Viewer Response 函数修复
修复Cookie属性、Set-Cookie头写入逻辑,异常场景不中断正常响应,修复后代码如下:
exports.handler = async (event, context) => { try { const response = event.Records[0].cf.response; const request = event.Records[0].cf.request; const headers = response.headers; const headerNameSrc = 'X-Amz-Meta-Last-Modified'; const headerNameDst = 'Last-Modified'; if (headers[headerNameSrc.toLowerCase()]) { headers[headerNameDst.toLowerCase()] = [{ key: headerNameDst, value: headers[headerNameSrc.toLowerCase()][0].value, }]; } // 仅当querystring携带jwt时才写入Cookie const queryParams = new URLSearchParams(request.querystring); const token = queryParams.get('jwt'); if (token) { // 补全Path=/确保全站生效;同站访问用SameSite=Lax即可,跨站携带Cookie再改回SameSite=None const cookieValue = `token=${token}; Path=/; Secure; HttpOnly; Max-Age=18000; SameSite=Lax`; // 追加Set-Cookie头,不覆盖源站返回的其他Cookie const existingSetCookie = headers['set-cookie'] || []; existingSetCookie.push({ key: "Set-Cookie", value: cookieValue }); headers['set-cookie'] = existingSetCookie; } return response; } catch (error) { console.log("Viewer response error:", error); // 响应阶段逻辑异常时返回正常源站响应,不中断业务 return event.Records[0].cf.response; } };
验证注意事项
- 测试时必须使用HTTPS协议访问CloudFront分发地址,HTTP场景下Secure标记的Cookie不会被浏览器存储。
- 首次访问带jwt参数的地址后,打开浏览器开发者工具-应用-Cookie面板,检查对应域名下是否写入
token项,确认Path、有效期、SameSite属性符合预期。 - 跨域调用场景下,前端请求需开启凭证携带配置(如axios的
withCredentials: true),否则跨域请求不会自动携带Cookie。
内容的提问来源于stack exchange,提问作者T.Demirer
相关产品推荐
相关产品推荐

