TypeScript 中的 `Cookie`

TypeScript 中的 `Cookie`

本文讲解 TypeScript 中的 Cookie

我们将介绍在浏览器和服务器端安全且可靠地处理 Cookie 的实用模式。

YouTube Video

TypeScript 中的 Cookie

Cookie 的基本概念

Cookie 是在客户端存储小字符串(name=value 键值对)的一种机制,可通过 Set-Cookie HTTP 头或 document.cookie 创建。可以通过安全属性(HttpOnlySecureSameSite 等)来控制其行为。

浏览器中的基本操作:document.cookie

下面是在浏览器中写入 Cookie 的最小示例。通过向 document.cookie 追加字符串来创建 Cookie。

 1// Set a simple cookie that expires in 7 days.
 2// Note: Comments are in English per the user's preference.
 3const setSimpleCookie = (name: string, value: string) => {
 4  const days = 7;
 5  const expires = new Date(Date.now() + days * 86400_000).toUTCString();
 6  // name=value; Expires=...; Path=/
 7  document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; Expires=${expires}; Path=/`;
 8};
 9
10setSimpleCookie('theme', 'dark');
  • 此代码创建一个 7 天后过期的 Cookie。在浏览器端使用很方便,但由于无法设置 HttpOnly,应避免存储敏感信息。

读取 Cookie(浏览器)

下面的函数是一个辅助工具,用于从 document.cookie 中获取指定名称的 cookie 的值。document.cookie 返回的是一个由分号和空格('; ')分隔的字符串。因此,我们对其进行分割并查找所需的 cookie。

 1// Parse document.cookie and return value for given name or null if not found.
 2const getCookie = (name: string): string | null => {
 3  const cookies = document.cookie ? document.cookie.split('; ') : [];
 4  for (const cookie of cookies) {
 5    const [k, ...rest] = cookie.split('=');
 6    const v = rest.join('=');
 7    if (decodeURIComponent(k) === name) {
 8      return decodeURIComponent(v);
 9    }
10  }
11  return null;
12};
13
14// Example usage:
15const theme = getCookie('theme'); // => "dark" if set
16console.log('theme cookie:', theme);
  • 该函数会对 cookie 的名称和值进行解码,以安全地进行比较和获取。它能够处理重复的 cookie 名称和被编码的特殊字符,是一种简单而健壮的实现。

理解并应用安全属性

Cookie 有几个重要的属性,它们分别控制安全性和行为范围。

  • HttpOnly 属性可防止通过 JavaScript 访问该 Cookie,有助于缓解 XSS(跨站脚本)攻击。注意,无法在浏览器端设置 HttpOnly
  • Secure 属性将 Cookie 限制为仅通过 HTTPS 发送,从而降低被窃听和篡改的风险。
  • SameSite 属性控制跨站请求是否携带 Cookie,并有助于防止 CSRF(跨站请求伪造)攻击。
  • Path 属性指定发送 Cookie 的请求路径范围,Domain 属性指定 Cookie 生效的域名。
  • 通过设置 ExpiresMax-Age 属性,可以控制 Cookie 的过期时间。

在浏览器中添加 SameSite/Secure 的示例(可行的部分)

HttpOnly 无法在客户端设置。下面是一个在客户端添加 SameSiteSecure 的示例。但是,Secure 仅在 HTTPS 页面上生效。

 1// Set cookie with SameSite and Secure attributes (Secure only effective over HTTPS).
 2const setCookieWithAttributes = (name: string, value: string) => {
 3  const maxAge = 60 * 60 * 24 * 7; // 7 days in seconds
 4  // Note: HttpOnly cannot be set from JS; set it on server-side when you want to restrict JS access.
 5  document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; `
 6    + `Max-Age=${maxAge}; `
 7    + `Path=/; `
 8    + `SameSite=Lax; `
 9    + `Secure`;
10};
11
12setCookieWithAttributes('session_hint', 'true');
  • SameSite=Lax 是一种安全且常见的选择。若需要更严格的限制可使用 SameSite=Strict,但来自外部站点的合法跳转可能会失效。

在服务器端设置 Cookie(Set-Cookie)(Node / TypeScript)

在服务器端可以通过 Set-Cookie 头添加 HttpOnly,因此会话管理理想情况下应在服务器端完成。下面是一个使用 Node 的 http 模块的示例。

 1// A minimal Node HTTP server in TypeScript that sets a secure HttpOnly cookie.
 2// This example uses built-in 'crypto' for a random session id.
 3import http from 'http';
 4import crypto from 'crypto';
 5
 6const server = http.createServer((req, res) => {
 7  if (req.url === '/login') {
 8    const sessionId = crypto.randomBytes(16).toString('hex');
 9    // Set cookie with HttpOnly, Secure, SameSite and Path
10    // Expires is optional — Max-Age preferred for relative lifetimes.
11    res.setHeader('Set-Cookie', `sid=${sessionId}; `
12      + `HttpOnly; `
13      + `Secure; `
14      + `SameSite=Strict; `
15      + `Path=/; `
16      + `Max-Age=3600`
17    );
18    res.writeHead(302, { Location: '/' });
19    res.end();
20    return;
21  }
22
23  res.writeHead(200, { 'Content-Type': 'text/plain' });
24  res.end('Hello\n');
25});
26
27server.listen(3000, () => {
28  console.log('Server running on http://localhost:3000');
29});
  • 当你在服务端设置 HttpOnly 属性时,Cookie 将无法被 JavaScript 访问,从而使 XSS(跨站脚本)攻击更难窃取它。此外,通过添加 Secure 属性使 Cookie 始终通过 HTTPS 发送,可以防止窃听和篡改,提升通信安全性。

Cookie 的序列化/解析实现(服务器端辅助工具)

下面是一个在服务器端构建 Set-Cookie 响应头字符串的简单工具。

 1// Cookie serialization helper for server-side use.
 2// Returns a properly formatted Set-Cookie header value.
 3type CookieOptions = {
 4  path?: string;
 5  domain?: string;
 6  maxAge?: number;
 7  expires?: Date;
 8  httpOnly?: boolean;
 9  secure?: boolean;
10  sameSite?: 'Strict' | 'Lax' | 'None';
11};
12
13const serializeCookie = (name: string, value: string, opts: CookieOptions = {}): string => {
14  const parts: string[] = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
15
16  if (opts.maxAge != null) parts.push(`Max-Age=${Math.floor(opts.maxAge)}`);
17  if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);
18  if (opts.domain) parts.push(`Domain=${opts.domain}`);
19  parts.push(`Path=${opts.path ?? '/'}`);
20  if (opts.httpOnly) parts.push('HttpOnly');
21  if (opts.secure) parts.push('Secure');
22  if (opts.sameSite) parts.push(`SameSite=${opts.sameSite}`);
23
24  return parts.join('; ');
25};
26
27// Example usage:
28const headerValue = serializeCookie('uid', 'abc123', {
29  httpOnly: true,
30  secure: true,
31  sameSite: 'Lax',
32  maxAge: 3600
33});
34console.log(headerValue);
35// => "uid=abc123; Max-Age=3600; Path=/; HttpOnly; Secure; SameSite=Lax"
  • 此工具可作为在自定义服务器上创建 Set-Cookie 的基础。库可以处理更多边界情况。

通过签名(HMAC)防篡改

将重要值直接存入 Cookie 是危险的。我们介绍一种在服务器端对值进行签名以检测篡改的方法。此处使用 HMAC-SHA256。在生产环境中,还需要在服务器端管理令牌的吊销。

 1// A simple HMAC signing and verification helper for cookies.
 2// Signing prevents client from tampering cookie values.
 3import crypto from 'crypto';
 4
 5const SECRET = 'replace_with_env_secret'; // store in env var in production
 6
 7const signValue = (value: string) => {
 8  const hmac = crypto.createHmac('sha256', SECRET);
 9  hmac.update(value);
10  return `${value}.${hmac.digest('hex')}`;
11};
12
13const verifySignedValue = (signed: string): string | null => {
14  const idx = signed.lastIndexOf('.');
15  if (idx === -1) return null;
16  const value = signed.slice(0, idx);
17  const sig = signed.slice(idx + 1);
18
19  const hmac = crypto.createHmac('sha256', SECRET);
20  hmac.update(value);
21  const expected = hmac.digest('hex');
22  // use timing-safe comparison in production
23  if (crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
24    return value;
25  }
26  return null;
27};
28
29// Example usage:
30const signed = signValue('userId=42');
31console.log('signed:', signed);
32console.log('verified:', verifySignedValue(signed)); // => "userId=42"
  • 通过这种方法,可以检查 Cookie 的值是否被篡改。不要将签名密钥包含在源代码中;应通过环境变量等安全方式进行管理。

Cookie 与 CSRF 缓解(通用模式)

使用 Cookie 的会话需要 CSRF 保护。典型的缓解措施包括:。

  • SameSite 设为 LaxStrict,以防止不必要的跨站发送。仅在确有跨域通信必要时使用 SameSite=None; Secure
  • 对表单和 API 请求使用由服务器验证的 CSRF 令牌。不要将令牌存储在 HttpOnly Cookie 中;相反,通过响应正文或 meta 标签提供它,并让 JavaScript 仅在必要时读取
  • 如果令牌可被 JavaScript 访问,请务必同时实施 XSS 防护(CSP、输出转义等)。
  • 对敏感操作以及在登录时要求重新认证,并防止会话固定攻击。

下面是一个安全的客户端示例,它在请求头中发送 CSRF 令牌。这假设客户端已经从服务器获取了令牌,例如在响应正文中。

 1// Example of sending CSRF token safely in a header using fetch.
 2// Assumes CSRF token was provided securely (e.g., via response body or meta tag).
 3async function sendProtectedRequest(url: string, csrfToken: string) {
 4  const res = await fetch(url, {
 5    method: 'POST',
 6    credentials: 'same-origin', // include cookies for same-site requests
 7    headers: {
 8      'Content-Type': 'application/json',
 9      'X-CSRF-Token': csrfToken
10    },
11    body: JSON.stringify({ action: 'doSomething' })
12  });
13  return res;
14}
  • 如果指定 credentials: 'same-origin',则只会发送同源的 Cookie。在服务器端,验证 X-CSRF-Token 请求头的值,并核对令牌是否匹配。如果确实需要跨域请求,请谨慎配置 CORS。

跨站(CORS)与 credentials 的关系

若要在跨域通信中发送和接收 Cookie,客户端必须指定 credentials: 'include',且服务器必须设置 Access-Control-Allow-Credentials: true。但是,应将此配置限制为受信任的来源,不要使用 Access-Control-Allow-Origin: *

 1async function fetchCrossOriginData() {
 2  // Example: cross-origin fetch sending cookies.
 3  // Server must set Access-Control-Allow-Credentials: true
 4  // and a specific trusted origin.
 5  const res = await fetch('https://api.example.com/data', {
 6    credentials: 'include', // send cookies only to trusted domains
 7    method: 'GET'
 8  });
 9  return res;
10}
  • 从安全角度看,CORS 与 Cookie 的组合十分棘手。使用白名单严格管理允许的来源,并避免不必要的跨域通信。此外,在使用 SameSite=None; Secure 时,强制使用 HTTPS 以防止中间人攻击。

示例:在 Express 中的安全会话 Cookie 设置(TypeScript)

在实践中,使用 Express、cookie 库、express-session 等。下面是一个使用 expresscookie-parser 的简单示例。在实践中,将 secure 设为 true,并通过环境变量管理 secret

 1// Express example using cookie-parser and setting a secure httpOnly cookie.
 2// npm install express cookie-parser @types/express @types/cookie-parser
 3import express from 'express';
 4import cookieParser from 'cookie-parser';
 5
 6const app = express();
 7app.use(cookieParser(process.env.COOKIE_SECRET));
 8
 9app.post('/login', (req, res) => {
10  // authenticate user (omitted)
11  const sessionId = 'generated-session-id';
12  res.cookie('sid', sessionId, {
13    httpOnly: true,
14    secure: true,        // require HTTPS in production
15    sameSite: 'lax',
16    maxAge: 1000 * 60 * 60 // 1 hour
17  });
18  res.json({ ok: true });
19});
20
21app.listen(3000);
  • 通过使用 cookieParsersecret 功能,可以轻松处理签名的 cookie。但是,在实际应用中,从安全性和可扩展性的角度出发,不应直接将数据存储在 cookie 中;而应使用专用的会话存储。

Cookie 前缀 __Host-__Secure-

浏览器会对某些前缀强制执行特殊规则。

  • __Secure- 前缀 如果 Cookie 名称以 __Secure- 开头,则必须带有 Secure 属性。
  • __Host- 前缀 如果以 __Host- 开头,则必须带 Secure,路径必须为 /(根),且不得设置 Domain

使用这些前缀可减少误配置并提升安全性。

Cookie 最佳实践

要安全地处理 Cookie,请考虑以下几点。

  • 不要在 Cookie 中直接存储敏感信息。访问令牌应优先使用服务器端会话 ID。
  • 为会话 Cookie 设置 HttpOnlySecureSameSite=Lax(或 Strict)。
  • 利用 __Host-__Secure- 等前缀。
  • 考虑使用签名(HMAC)与加密以防篡改和窃听。
  • 启用 Secure 并强制使用 HTTPS。
  • 遵循最小权限并设置较短的过期时间。
  • 使用 CSRF 令牌。
  • 注意不同浏览器(尤其是旧版本)在 SameSite 行为上的差异。

关于 Cookie 的常见误解

关于 Cookie,请注意以下常见误区。

  • '添加 HttpOnly 就能消除 XSS 影响。' 尽管 HttpOnly 可以阻止 JavaScript 访问 Cookie,XSS 仍然可以被用来发起任意请求。还应使用 CSRF 令牌校验、CSP(内容安全策略)以及输入净化。
  • '本地开发不需要 Secure。' 即便在本地环境中模拟 HTTPS 并验证相关行为,也能提高测试的准确性。至少应在预发布和生产环境中使用 HTTPS。
  • '较长的过期时间更方便。' 如果将 Cookie 的有效期设置得很长,一旦被窃取,可被滥用的时间窗口也会随之增加。可以设置更短的过期时间,并配合定期重新认证与令牌轮换。

总结

尽管 Cookie 易于使用,但处理不当会引入安全漏洞。要在 TypeScript 中正确管理它们,重要的是理解 HttpOnlySecureSameSite 等属性,并在服务器端强制执行安全设置。通过不直接存储敏感数据,并将签名与短期有效期结合使用,可以实现安全且可靠的会话管理。

您可以在我们的YouTube频道上使用Visual Studio Code跟随上述文章进行学习。 请也查看我们的YouTube频道。

YouTube Video