ZDecode
Javascript

cookie

传统前后端不分离项目中的使用场景

在传统的前后端不分离项目中,Cookie 是服务器与客户端之间状态管理的核心机制。这些项目通常采用服务器端渲染(SSR),页面由服务器直接生成并返回给浏览器。以下是传统架构中 Cookie 的主要使用场景:

1. 会话管理与用户认证

这是最基础也是最常见的使用场景:

// PHP 示例 - 登录处理
session_start();
if ($_POST['username'] == 'admin' && $_POST['password'] == 'secret') {
  $_SESSION['user_id'] = 1;
  $_SESSION['username'] = 'admin';
  $_SESSION['is_authenticated'] = true;
  
  // 设置会话 Cookie
  setcookie(session_name(), session_id(), [
    'expires' => time() + 86400,
    'path' => '/',
    'secure' => true,
    'httponly' => true
  ]);
  
  header('Location: /dashboard.php');
  exit;
}
// Java Servlet 示例
HttpSession session = request.getSession();
session.setAttribute("userId", user.getId());
session.setAttribute("username", user.getUsername());

// 页面访问控制
if (session.getAttribute("userId") == null) {
  response.sendRedirect("/login");
  return;
}

2. 记住用户登录状态

"记住我"功能允许用户在关闭浏览器后仍保持登录状态:

// PHP - 设置持久化登录 Cookie
if (isset($_POST['remember_me']) && $_POST['remember_me'] == 'on') {
  $token = bin2hex(random_bytes(32));
  $user_id = $user['id'];
  
  // 存储到数据库
  $stmt = $db->prepare("INSERT INTO persistent_logins (user_id, token, expires) VALUES (?, ?, ?)");
  $expires = time() + (30 * 24 * 60 * 60); // 30天
  $stmt->execute([$user_id, $token, date('Y-m-d H:i:s', $expires)]);
  
  setcookie('remember_user', $token, [
    'expires' => $expires,
    'path' => '/',
    'httponly' => true,
    'secure' => true
  ]);
}

// 登录检查
function checkRememberMeCookie() {
  if (!isset($_SESSION['user_id']) && isset($_COOKIE['remember_user'])) {
    $token = $_COOKIE['remember_user'];
    
    $stmt = $db->prepare("SELECT user_id FROM persistent_logins WHERE token = ? AND expires > NOW()");
    $stmt->execute([$token]);
    $result = $stmt->fetch();
    
    if ($result) {
      // 自动登录用户
      $user = getUserById($result['user_id']);
      $_SESSION['user_id'] = $user['id'];
      $_SESSION['username'] = $user['username'];
    }
  }
}

3. 购物车管理

在电子商务网站中使用 Cookie 跟踪购物车状态:

// 添加产品到购物车
if (isset($_POST['add_to_cart'])) {
  $product_id = $_POST['product_id'];
  $quantity = $_POST['quantity'];
  
  session_start();
  if (!isset($_SESSION['cart'])) {
    $_SESSION['cart'] = [];
  }
  
  if (isset($_SESSION['cart'][$product_id])) {
    $_SESSION['cart'][$product_id] += $quantity;
  } else {
    $_SESSION['cart'][$product_id] = $quantity;
  }
  
  header('Location: /cart.php');
  exit;
}

// 显示购物车页面
session_start();
$cart = $_SESSION['cart'] ?? [];
$cart_items = [];

foreach ($cart as $product_id => $quantity) {
  $product = getProductById($product_id);
  $cart_items[] = [
    'id' => $product_id,
    'name' => $product['name'],
    'price' => $product['price'],
    'quantity' => $quantity,
    'total' => $product['price'] * $quantity
  ];
}

// 在模板中显示购物车
require 'templates/cart.php';

4. 防止 CSRF 攻击

传统应用中,CSRF 令牌通常与会话关联并在表单中使用:

// 生成 CSRF 令牌并存储在会话中
function generateCsrfToken() {
  if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
  }
  return $_SESSION['csrf_token'];
}

// 在表单中输出 CSRF 令牌
<form method="POST" action="/process-form.php">
  <input type="hidden" name="csrf_token" value="<?php echo generateCsrfToken(); ?>">
  <!-- 其他表单字段 -->
  <button type="submit">提交</button>
</form>

// 验证表单提交
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
  if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
    die('CSRF 验证失败');
  }
  
  // 处理表单数据...
}

5. 网站本地化和用户首选项

存储用户的语言、主题等首选项:

// 处理语言切换
if (isset($_GET['lang'])) {
  $lang = $_GET['lang'];
  if (in_array($lang, ['en', 'fr', 'de', 'es'])) {
    setcookie('user_lang', $lang, [
      'expires' => time() + 365 * 24 * 60 * 60,
      'path' => '/'
    ]);
  }
  header('Location: ' . $_SERVER['HTTP_REFERER']);
  exit;
}

// 设置页面语言
$lang = $_COOKIE['user_lang'] ?? 'en';
require_once "languages/{$lang}.php";
<!-- JSP 中应用用户首选项 -->
<%
String theme = "default";
Cookie[] cookies = request.getCookies();
if (cookies != null) {
  for (Cookie cookie : cookies) {
    if (cookie.getName().equals("site_theme")) {
      theme = cookie.getValue();
      break;
    }
  }
}
%>
<link rel="stylesheet" href="/css/themes/<%= theme %>.css">

6. 追踪访问统计

使用 Cookie 跟踪用户行为和访问模式:

// 追踪访问统计
if (!isset($_COOKIE['visitor_id'])) {
  $visitor_id = uniqid();
  setcookie('visitor_id', $visitor_id, [
    'expires' => time() + 365 * 24 * 60 * 60,
    'path' => '/'
  ]);
  
  // 记录新访客
  $stmt = $db->prepare("INSERT INTO visitors (visitor_id, first_visit) VALUES (?, NOW())");
  $stmt->execute([$visitor_id]);
} else {
  $visitor_id = $_COOKIE['visitor_id'];
  
  // 更新访问记录
  $stmt = $db->prepare("UPDATE visitors SET visit_count = visit_count + 1, last_visit = NOW() WHERE visitor_id = ?");
  $stmt->execute([$visitor_id]);
}

// 记录页面访问
$page = $_SERVER['REQUEST_URI'];
$stmt = $db->prepare("INSERT INTO page_views (visitor_id, page, view_date) VALUES (?, ?, NOW())");
$stmt->execute([$visitor_id, $page]);

7. 表单数据自动填充

保存用户填写的表单数据,防止因页面刷新丢失:


// 保存表单数据到 Cookie
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
  $contact_info = [
    'name' => $_POST['name'] ?? '',
    'email' => $_POST['email'] ?? '',
    'phone' => $_POST['phone'] ?? ''
  ];
  
  setcookie('saved_contact_form', json_encode($contact_info), [
    'expires' => time() + 3600, // 1小时
    'path' => '/'
  ]);
}

// 在表单中预填数据
$saved_data = [];
if (isset($_COOKIE['saved_contact_form'])) {
  $saved_data = json_decode($_COOKIE['saved_contact_form'], true);
}
?>

<form method="POST">
  <input type="text" name="name" value="<?php echo htmlspecialchars($saved_data['name'] ?? ''); ?>">
  <input type="email" name="email" value="<?php echo htmlspecialchars($saved_data['email'] ?? ''); ?>">
  <input type="tel" name="phone" value="<?php echo htmlspecialchars($saved_data['phone'] ?? ''); ?>">
  <button type="submit">提交</button>
</form>

8. 实现向导式表单

多步骤表单流程使用 Cookie 保存中间状态:

// 第一步表单处理
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['step1'])) {
  $_SESSION['registration'] = [
    'personal_info' => [
      'name' => $_POST['name'],
      'email' => $_POST['email'],
      'dob' => $_POST['dob']
    ]
  ];
  header('Location: /register-step2.php');
  exit;
}

// 第二步表单处理
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['step2'])) {
  $_SESSION['registration']['address'] = [
    'street' => $_POST['street'],
    'city' => $_POST['city'],
    'postal_code' => $_POST['postal_code'],
    'country' => $_POST['country']
  ];
  header('Location: /register-step3.php');
  exit;
}

// 最终步骤 - 完成注册
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['complete'])) {
  $registration_data = $_SESSION['registration'];
  $registration_data['preferences'] = [
    'newsletter' => isset($_POST['newsletter']),
    'promotions' => isset($_POST['promotions'])
  ];
  
  // 处理注册数据...
  registerUser($registration_data);
  
  // 清除会话数据
  unset($_SESSION['registration']);
  
  header('Location: /registration-success.php');
  exit;
}

9. 访问控制和权限管理

基于角色的访问控制:

// 检查用户权限
function checkPermission($permission) {
  // 用户未登录
  if (!isset($_SESSION['user_id'])) {
    return false;
  }
  
  // 从会话中获取用户角色
  $role = $_SESSION['user_role'] ?? 'guest';
  
  // 获取角色权限
  $permissions = getRolePermissions($role);
  
  return in_array($permission, $permissions);
}

// 在访问页面或执行操作前检查
if (!checkPermission('manage_users')) {
  header('Location: /access-denied.php');
  exit;
}

10. 维护应用状态

在页面间保持持续的应用状态:

// 保存搜索过滤器
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['search'])) {
  $_SESSION['product_filters'] = [
    'category' => $_POST['category'] ?? 'all',
    'min_price' => $_POST['min_price'] ?? '',
    'max_price' => $_POST['max_price'] ?? '',
    'sort_by' => $_POST['sort_by'] ?? 'popularity'
  ];
}

// 在搜索页面使用过滤器
$filters = $_SESSION['product_filters'] ?? [
  'category' => 'all',
  'min_price' => '',
  'max_price' => '',
  'sort_by' => 'popularity'
];

$products = searchProducts(
  $filters['category'],
  $filters['min_price'],
  $filters['max_price'],
  $filters['sort_by']
);

// 在模板中显示产品和应用的过滤器
require 'templates/product-search.php';

前后端分离项目中的应用

在前后端分离的现代架构中,虽然 Cookie 的使用模式有所变化,但它仍然有许多实际应用。以下是 Cookie 在前后端分离项目中的几种重要应用场景:

1. 跨域认证方案

前后端分离项目中,API 服务器和前端应用通常部署在不同域名下,这时 Cookie 可以这样使用:

使用 Cookie + CORS

// 后端 Node.js Express 设置
app.use(cors({
  origin: 'https://frontend.example.com',  // 前端域名
  credentials: true  // 允许跨域请求携带凭证
}));

app.post('/login', (req, res) => {
  // 认证逻辑...
  res.cookie('authToken', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'none',  // 允许跨站发送
    maxAge: 24 * 60 * 60 * 1000  // 24小时
  });
  res.json({ success: true });
});

// 前端 Axios 配置
axios.defaults.withCredentials = true;  // 允许请求携带 Cookie

2.访问令牌与刷新令牌分离存储

在 JWT 认证体系中,将 Refresh Token 存储在 HttpOnly Cookie 中,而 Access Token 存储在内存中:

// 后端设置
app.post('/login', (req, res) => {
  // 验证用户身份...
  const accessToken = generateAccessToken(user);
  const refreshToken = generateRefreshToken(user);
  
  // refreshToken 存储在 HttpOnly Cookie 中
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    path: '/api/refresh',  // 限制 Cookie 只在刷新令牌路径可用
    maxAge: 30 * 24 * 60 * 60 * 1000  // 30天
  });
  
  // accessToken 返回给前端存储在内存中
  res.json({ accessToken });
});

// 前端处理
function login(credentials) {
  return api.post('/login', credentials)
    .then(response => {
      // 将 accessToken 存储在内存中
      sessionStorage.setItem('accessToken', response.data.accessToken);
      return response;
    });
}

3.CSRF 防护令牌

即使在前后端分离架构中,CSRF 攻击仍然是潜在威胁,Cookie 可用于存储 CSRF 令牌:

// 后端生成 CSRF 令牌
app.use((req, res, next) => {
  const csrfToken = generateRandomToken();
  res.cookie('XSRF-TOKEN', csrfToken, {
    secure: true,
    sameSite: 'strict'
  });
  next();
});

// 前端自动从 Cookie 中提取并添加到请求头
// 例如 Angular 自动从名为 XSRF-TOKEN 的 Cookie 中提取令牌
// 并添加到 X-XSRF-TOKEN 请求头

4. 用户首选项和个性化设置

非敏感的用户偏好设置可以存储在常规 Cookie 中:

// 前端设置主题偏好
document.cookie = `theme=${selectedTheme}; max-age=${60*60*24*365}; path=/`;

// 从 Cookie 读取主题
function getThemePreference() {
  const cookies = document.cookie.split(';');
  const themeCookie = cookies.find(cookie => cookie.trim().startsWith('theme='));
  return themeCookie ? themeCookie.split('=')[1] : 'light';
}

5. 分布式会话管理

在微服务架构中,Cookie 可以用于在不同服务之间共享会话标识符:

// 网关服务设置会话 Cookie
app.post('/login', (req, res) => {
  const sessionId = generateUniqueSessionId();
  
  // 在 Redis 中存储会话数据
  redisClient.set(`session:${sessionId}`, JSON.stringify(userData));
  
  // 设置会话 Cookie
  res.cookie('sessionId', sessionId, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict'
  });
  
  res.json({ success: true });
});

// 各微服务使用会话 ID 获取用户数据
function getUserFromSession(req) {
  const sessionId = req.cookies.sessionId;
  return redisClient.get(`session:${sessionId}`);
}

6. 实现"记住我"功能

app.post('/login', (req, res) => {
  // 常规认证逻辑...
  
  if (req.body.rememberMe) {
    // 生成持久化令牌
    const persistentToken = generateSecureToken();
    
    // 存储到数据库,关联用户
    saveTokenToDatabase(persistentToken, userId);
    
    // 设置长期有效的 Cookie
    res.cookie('rememberMe', persistentToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: 30 * 24 * 60 * 60 * 1000  // 30天
    });
  }
  
  res.json({ success: true });
});

7. A/B 测试和特性标记

// 设置用户实验组
app.use((req, res, next) => {
  if (!req.cookies.experimentGroup) {
    const group = Math.random() > 0.5 ? 'A' : 'B';
    res.cookie('experimentGroup', group, { maxAge: 30 * 24 * 60 * 60 * 1000 });
  }
  next();
});

// 前端检查用户实验组
function getExperimentalFeatures() {
  const experimentGroup = getCookie('experimentGroup');
  if (experimentGroup === 'A') {
    return { newHeader: true, darkMode: false };
  } else {
    return { newHeader: false, darkMode: true };
  }
}

8. 会话状态恢复

即使在使用 JWT 的情况下,Cookie 也可用于在浏览器刷新后恢复会话状态:

// 登录成功后存储关键会话信息
function storeSessionInfo(sessionData) {
  document.cookie = `sessionState=${JSON.stringify({
    userId: sessionData.userId,
    lastPage: window.location.pathname,
    timestamp: Date.now()
  })}; path=/; max-age=3600; SameSite=Strict`;
}

// 页面加载时恢复会话
function restoreSession() {
  const sessionCookie = document.cookie
    .split('; ')
    .find(row => row.startsWith('sessionState='));
    
  if (sessionCookie) {
    const sessionState = JSON.parse(sessionCookie.split('=')[1]);
    // 恢复应用状态
    redirectToLastPage(sessionState.lastPage);
  }
}