← 中文资源

简体中文版本

TLS 会话恢复

TLS 会话恢复通过复用先前协商的会话状态缩短后续握手:会话 ID、会话票据或 QUIC 与 TLS 1.3 的 0-RTT 早期数据,可省一到两个往返。

阅读英文原文

概念定义

完整 TLS 握手在发出首个应用字节前要消耗一到两个往返。会话恢复允许双方跳过昂贵部分:客户端出示服务器先前签发的票据或 PSK,即可证明旧的会话状态。恢复的连接继承原有密码套件决策与身份假设。

工作机制

TLS 1.2 中服务器要么按会话 ID 保存状态,要么把状态加密进票据交给客户端。TLS 1.3 把恢复从握手中分离并加入早期数据:客户端在第 0 飞行就发送用票据派生密钥加密的应用数据。服务器维护重放窗口以拒绝重复。

使用场景

API 网关、移动后端与 CDN 边缘应启用票据恢复并高频轮换密钥。0-RTT 适合重放无害的幂等 GET 流量。长寿命的内部连接收益较小,因为握手成本本就被摊销。

常见误区

票据密钥就是恢复流量的解密密钥,轮换频率直接限定泄露的影响面,这是安全要求而非锦上添花。0-RTT 数据可被攻击者重放,非幂等操作不得在无反重放跟踪时信任它。负载均衡器把票据池跨后端共享时,还可能意外泄漏会话亲和性假设。

代码示例

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

.animate-fade {
  animation: fadeIn 0.3s ease-out forwards;
}

@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.05); }
}
export function parseContentDisposition(header) {
  if (typeof header !== "string" || header.trim() === "") {
    throw new TypeError("header must be a non-empty string");
  }
  const segments = splitParameters(header);
  const type = segments.shift().trim().toLowerCase();
  if (type !== "inline" && type !== "attachment") {
    throw new Error(`unsupported disposition type "${type}"`);
  }
  const parameters = new Map();
  for (const segment of segments) {
    const equals = segment.indexOf("=");
    if (equals === -1) continue;
    const name = segment.slice(0, equals).trim().toLowerCase();
    if (name === "") continue;
    parameters.set(name, unquote(segment.slice(equals + 1).trim()));
  }
  return { type, filename: resolveFilename(parameters), parameters };
}

function splitParameters(header) {
  const segments = [];
  let current = "";
  let quoted = false;
  for (const character of header) {
    if (character === '"') quoted = !quoted;
    if (character === ";" && !quoted) {
      segments.push(current);
      current = "";
      continue;
    }
    current += character;
  }
  segments.push(current);
  return segments;
}

function unquote(value) {
  if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
    return value.slice(1, -1).replace(/\\(.)/g, "$1");
  }
  return value;
}

function resolveFilename(parameters) {
  const extended = parameters.get("filename*");
  if (extended !== undefined) {
    const match = /^([\w-]+)'([\w-]*)'(.+)$/.exec(extended);
    if (match !== null) {
      try {
        return sanitize(decodeURIComponent(match[3]));
      } catch {
        return sanitize(extended);
      }
    }
  }
  const plain = parameters.get("filename");
  return plain === undefined ? null : sanitize(plain);
}

function sanitize(filename) {
  return filename.split(/[\\/]/).pop() || null;
}

console.log(parseContentDisposition('attachment; filename="report.pdf"'));
console.log(parseContentDisposition("attachment; filename*=UTF-8''%E4%B8%AD%E6%96%87.pdf"));
console.log(parseContentDisposition('inline; filename="../../etc/passwd"'));