Web学习之用户认证

一、Cookie 和 Session 的存储内容对比

  1. Cookie 中存储什么信息
    本质:Cookie 只存储一个 Session ID(会话标识符)

具体内容:

javascript 复制代码
// 一个典型的 Cookie 示例(开发者工具中查看)
Name: sessionId
Value: s%3Aabc123def456.session_secret_signature
// 解码后:s:abc123def456.session_secret_signature
// s: 是 express-session 的前缀,abc123def456 是真正的 session ID

// Cookie 的其他关键字段:
Domain: .example.com    // 作用域
Path: /                 // 路径
Expires/Max-Age: 时间戳  // 过期时间
Secure: true           // 仅 HTTPS
HttpOnly: true         // 防止 JS 访问
SameSite: Lax          // 跨站策略

为什么 Cookie 只存 ID?

安全考虑:Cookie 存储在用户浏览器,可能被窃取

大小限制:Cookie 一般限制在 4KB

网络性能:每次请求都会自动发送 Cookie

  1. Session 中存储什么信息
    本质:Session 存储在服务器端,保存用户的完整会话数据

具体内容:

javascript 复制代码
// 一个典型的 session 数据结构(存储在 Redis 中)
{
  "sess:abc123def456": {
    "userId": "123456",
    "username": "john_doe",
    "email": "john@example.com",
    "roles": ["user", "admin"],
    "loginTime": "2024-01-15T10:30:00Z",
    "lastActivity": "2024-01-15T11:15:00Z",
    "cart": [
      { "productId": "p001", "quantity": 2 },
      { "productId": "p002", "quantity": 1 }
    ],
    "preferences": {
      "theme": "dark",
      "language": "zh-CN",
      "notifications": true
    },
    "csrfToken": "x678y9z0",  // 防止 CSRF 攻击
    "_expire": 1705313700000   // 过期时间戳
  }
}

Session 中常见的数据类型:

认证信息:userId, username, email

权限信息:roles, permissions

会话状态:loginTime, lastActivity

业务数据:购物车、表单草稿、临时数据

安全令牌:csrfToken, oauthState

用户偏好:主题、语言设置

二、存储位置详解

  1. Cookie 存储位置
    位置:客户端浏览器中

具体存储:

text 复制代码
浏览器存储位置:
├── 内存 Cookie(Session Cookie)
│   └── 浏览器关闭即删除
│
├── 硬盘 Cookie(持久 Cookie)
│   └── 按过期时间存储在硬盘
│
└── 按域和路径组织
    ├── example.com/
    │   ├── sessionId
    │   ├── userPref
    │   └── trackingId
    └── api.example.com/
        └── authToken

查看方式(浏览器开发者工具):

Chrome: Application → Storage → Cookies

Firefox: Storage → Cookies

命令行: document.cookie(仅限非 HttpOnly Cookie)

  1. Session 存储位置
    位置:服务器端

存储介质:

javascript 复制代码
// 1. 内存存储(默认,不推荐生产环境)
const session = require('express-session');
app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true
}));

// 2. Redis 存储(推荐生产环境)
const RedisStore = require('connect-redis')(session);
app.use(session({
  store: new RedisStore({
    host: 'localhost',
    port: 6379,
    prefix: 'sess:',  // Redis key 前缀
    ttl: 86400        // 过期时间(秒)
  }),
  secret: 'your-secret'
}));

// 3. 数据库存储(MySQL/PostgreSQL/MongoDB)
const MongoStore = require('connect-mongo');
app.use(session({
  store: MongoStore.create({
    mongoUrl: 'mongodb://localhost/sessions',
    ttl: 14 * 24 * 60 * 60 // 14天
  }),
  secret: 'your-secret'
}));

Redis 中的实际存储结构:

bash 复制代码
# 查看所有 session
redis-cli keys "sess:*"

# 查看具体 session 内容
redis-cli get "sess:abc123def456"
# 返回 JSON 字符串,包含所有 session 数据

三、完整实现流程详解

流程图

text 复制代码
客户端                    服务器
  |                         |
  |--- 1. 登录请求 --------->|
  |                         |
  |<-- 2. 创建 session -----|
  |    Set-Cookie: sessionId|
  |                         |
  |--- 3. 带 Cookie 请求 --->|
  |   Cookie: sessionId     |
  |                         |
  |<-- 4. 验证 session -----|
  |    返回 session 数据    |
  |                         |
  |--- 5. 后续请求 --------->|
  |   (自动携带 Cookie)     |
  |                         |
  |<-- 6. 验证/更新 session |
  |                         |
  |--- 7. 登出请求 --------->|
  |                         |
  |<-- 8. 销毁 session -----|
  |   清除 Cookie          |

详细步骤代码实现

javascript 复制代码
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);

const app = express();

// 1. Session 中间件配置
app.use(session({
  store: new RedisStore({
    host: '127.0.0.1',
    port: 6379,
    prefix: 'sess:',
    ttl: 1800 // 30分钟
  }),
  name: 'sessionId',           // Cookie 名称,默认 'connect.sid'
  secret: 'complex-secret-key-change-in-production',
  resave: false,              // 避免 session 被覆盖
  saveUninitialized: false,   // 不保存空的 session
  cookie: {
    // Cookie 相关设置
    maxAge: 30 * 60 * 1000,   // 30分钟(毫秒)
    secure: process.env.NODE_ENV === 'production', // 仅 HTTPS
    httpOnly: true,           // 防止 XSS
    sameSite: 'lax',          // CSRF 防护
    path: '/',                // Cookie 路径
    domain: '.example.com'    // 作用域
  },
  rolling: true,              // 每次请求重置过期时间
  genid: function(req) {
    // 生成唯一的 session ID
    return require('crypto').randomBytes(16).toString('hex');
  }
}));

// 2. 用户登录 - 创建 session
app.post('/api/login', async (req, res) => {
  const { username, password } = req.body;
  
  // 验证用户凭证
  const user = await authenticateUser(username, password);
  
  if (user) {
    // 在 session 中存储用户信息
    req.session.userId = user.id;
    req.session.username = user.username;
    req.session.roles = user.roles;
    req.session.loginTime = new Date();
    req.session.lastActivity = new Date();
    
    // 生成 CSRF token
    req.session.csrfToken = require('crypto')
      .randomBytes(32).toString('hex');
    
    // 设置购物车(如果之前有)
    if (!req.session.cart) {
      req.session.cart = [];
    }
    
    // 发送响应(Cookie 会自动通过 Set-Cookie 头发送)
    res.json({
      success: true,
      message: '登录成功',
      user: {
        id: user.id,
        username: user.username
      },
      // 如果需要,可以在响应中返回 CSRF token
      csrfToken: req.session.csrfToken
    });
  } else {
    res.status(401).json({ success: false, message: '认证失败' });
  }
});

// 3. Session 验证中间件
function requireAuth(req, res, next) {
  // 检查 session 是否存在且包含用户信息
  if (!req.session || !req.session.userId) {
    return res.status(401).json({ 
      error: '未授权,请先登录' 
    });
  }
  
  // 更新最后活动时间
  req.session.lastActivity = new Date();
  
  // 检查 CSRF token(对于 POST/PUT/DELETE 请求)
  if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
    const clientToken = req.headers['x-csrf-token'] || req.body._csrf;
    if (clientToken !== req.session.csrfToken) {
      return res.status(403).json({ error: 'CSRF token 无效' });
    }
  }
  
  // 将用户信息附加到请求对象
  req.user = {
    id: req.session.userId,
    username: req.session.username,
    roles: req.session.roles
  };
  
  next();
}

// 4. 受保护的路由
app.get('/api/profile', requireAuth, (req, res) => {
  // req.user 已在中间件中设置
  res.json({
    user: req.user,
    loginTime: req.session.loginTime,
    lastActivity: req.session.lastActivity
  });
});

// 5. 更新 session 数据(如购物车)
app.post('/api/cart/add', requireAuth, (req, res) => {
  const { productId, quantity } = req.body;
  
  // 确保购物车存在
  if (!req.session.cart) {
    req.session.cart = [];
  }
  
  // 查找商品是否已在购物车
  const existingItem = req.session.cart.find(
    item => item.productId === productId
  );
  
  if (existingItem) {
    existingItem.quantity += quantity;
  } else {
    req.session.cart.push({ productId, quantity });
  }
  
  // 手动保存 session(当修改了 session 时)
  req.session.save((err) => {
    if (err) {
      console.error('保存 session 失败:', err);
      return res.status(500).json({ error: '服务器错误' });
    }
    res.json({ 
      success: true, 
      cart: req.session.cart 
    });
  });
});

// 6. 用户登出 - 销毁 session
app.post('/api/logout', (req, res) => {
  // 获取 session ID(用于清理其他存储)
  const sessionId = req.session.id;
  
  // 销毁 session
  req.session.destroy((err) => {
    if (err) {
      console.error('销毁 session 失败:', err);
      return res.status(500).json({ error: '登出失败' });
    }
    
    // 清除客户端的 Cookie
    res.clearCookie('sessionId', {
      path: '/',
      domain: '.example.com'
    });
    
    // 可选:清理其他相关存储
    cleanupSessionData(sessionId);
    
    res.json({ success: true, message: '登出成功' });
  });
});

// 7. Session 清理任务(定时任务)
function cleanupExpiredSessions() {
  // Redis 会自动清理过期的 session(基于 TTL)
  // 对于数据库存储,可能需要定时任务
  setInterval(async () => {
    const expiredSessions = await SessionModel.find({
      expires: { $lt: new Date() }
    });
    
    // 清理过期 session
    await SessionModel.deleteMany({
      _id: { $in: expiredSessions.map(s => s._id) }
    });
    
    console.log(`清理了 ${expiredSessions.length} 个过期 session`);
  }, 3600000); // 每小时清理一次
}

四、关键字段和作用详解

字段 值示例 作用 重要性 详细说明
Name sessionId Cookie 名称 ★★★ 标识 cookie 的键名,服务器通过此名称读取 cookie 值
Value s%3Aabc123 加密的 session ID ★★★ 经过编码和签名的 session ID,是连接客户端和服务器的关键凭证
Domain .example.com 作用域 ★★★ 指定 cookie 有效的域名,. 开头表示所有子域名共享
Path / Cookie 有效的路径 ★★ 指定 cookie 在网站中的有效路径,/ 表示全站有效
Expires/Max-Age 2024-01-16T10:30:00Z 过期时间 ★★★ 控制 cookie 的存活时间,过期后浏览器自动删除
Secure true 仅通过 HTTPS 传输 ★★★ 防止 cookie 在明文中传输被窃取,生产环境必须启用
HttpOnly true 禁止 JavaScript 访问 ★★★ 防止 XSS 攻击窃取 cookie,敏感 cookie 必须设置
SameSite Lax/Strict CSRF 防护策略 ★★★ 控制跨站请求是否携带 cookie,有效防止 CSRF 攻击

Session 配置字段表

字段 默认值 作用 推荐设置 详细说明
name connect.sid Cookie 名称 自定义名称 避免使用默认名称,提高安全性,防止攻击者猜测
secret - 签名密钥 长且复杂的随机字符串 用于签名 session ID,防止篡改,定期更换
resave true 强制保存 session false 避免每次请求都保存 session,减少竞争条件
saveUninitialized true 保存空 session false 不保存未修改的 session,节省存储空间
cookie.secure false 仅 HTTPS 生产环境设为 true 确保 cookie 只在安全连接中传输
cookie.httpOnly true 防 XSS 始终 true 防止 JavaScript 访问敏感 cookie
cookie.sameSite false 跨站策略 lax 或 strict strict:完全禁止跨站;lax:部分允许
cookie.maxAge null Cookie 过期时间 根据业务设置 单位毫秒,如 30 分钟:30 * 60 * 1000
rolling false 重置过期时间 true 用户每次活动都延长 session 有效期
store 内存 Session 存储 Redis(生产环境) 生产环境必须使用外部存储,支持集群部署

Session 数据字段表

字段名 类型 存储内容 生命周期 详细说明
userId String 用户唯一标识 登录到登出 用于关联数据库中的用户记录,最关键的字段
username String 用户名 登录到登出 显示用途,避免频繁查询数据库
roles Array 用户角色权限 登录到登出 如 ["user", "admin"],用于权限控制
loginTime Date 登录时间 登录到登出 记录用户登录时刻,用于审计和安全分析
lastActivity Date 最后活动时间 每次请求更新 用于判断用户是否活跃,实现自动登出
csrfToken String CSRF 防护令牌 每次会话 随机生成的 token,防止跨站请求伪造攻击
cart Array 购物车数据 会话期间 存储用户临时的购物车信息,如 [{productId, quantity}]
preferences Object 用户偏好 会话期间 如主题、语言等个性化设置
tempData Any 临时数据 短期存储 表单数据、验证码等临时信息,定期清理

五、安全最佳实践

  1. Cookie 安全设置
javascript 复制代码
app.use(session({
  // ... 其他配置
  cookie: {
    secure: true,                    // 强制 HTTPS
    httpOnly: true,                  // 防 XSS
    sameSite: 'strict',              // 防 CSRF
    domain: '.yourdomain.com',       // 限制域名
    path: '/',                       // 限制路径
    maxAge: 30 * 60 * 1000           // 合理过期时间
  }
}));
  1. Session 数据清理
javascript 复制代码
// 定期清理过期 session
function cleanOldSessions() {
  // 删除超过30天未活动的 session
  const cutoff = Date.now() - (30 * 24 * 60 * 60 * 1000);
  
  // Redis 示例
  redisClient.keys('sess:*', (err, keys) => {
    keys.forEach(key => {
      redisClient.get(key, (err, sessionStr) => {
        if (sessionStr) {
          const session = JSON.parse(sessionStr);
          if (session.lastActivity < cutoff) {
            redisClient.del(key);
          }
        }
      });
    });
  });
}
  1. 敏感信息处理
javascript 复制代码
// 不要在 session 中存储敏感信息
// 错误示例:
req.session.passwordHash = user.passwordHash; // ❌ 危险!

// 正确做法:
req.session.userId = user.id; // ✅ 只存储标识符
req.session.roles = user.roles; // ✅ 存储非敏感信息

// 敏感信息从数据库实时获取
function getUserData(req, res, next) {
  if (req.session.userId) {
    User.findById(req.session.userId)
      .select('-password -salt') // 排除敏感字段
      .then(user => {
        req.user = user;
        next();
      });
  } else {
    next();
  }
}

六、常见问题排查

  1. Session 不持久
avascript 复制代码
// 检查点:
// 1. Cookie 是否设置正确
console.log('Cookie:', req.headers.cookie);

// 2. Session 是否保存成功
req.session.test = 'value';
req.session.save((err) => {
  if (err) console.error('保存失败:', err);
});

// 3. 检查存储连接
redisClient.ping((err, result) => {
  console.log('Redis 连接:', err ? '失败' : '正常');
});
2. 跨域问题
javascript
// 前端需要设置 withCredentials
fetch('/api/data', {
  credentials: 'include'  // 包含 Cookie
});

// 后端需要设置 CORS
app.use(cors({
  origin: 'https://frontend.com',
  credentials: true,  // 允许凭证
  methods: ['GET', 'POST', 'PUT', 'DELETE']
}));

总结

Cookie-Session 机制的核心是:

Cookie 只存 ID:安全、小巧、自动传输

Session 存数据:完整、安全、服务器可控

通过 ID 关联:建立客户端和服务器的信任桥梁

配合安全策略:HttpOnly、Secure、SameSite 多重防护

这种机制在传统 Web 应用中表现优秀,特别是需要服务器完全控制会话、需要存储大量临时数据、或需要支持服务器主动登出的场景。但在分布式系统和前后端分离架构中,需要考虑 Session 共享和扩展性问题

相关推荐
●VON2 小时前
React Native for OpenHarmony:项目目录结构与跨平台构建流程详解
javascript·学习·react native·react.js·架构·跨平台·von
We་ct2 小时前
LeetCode 36. 有效的数独:Set实现哈希表最优解
前端·算法·leetcode·typescript·散列表
AI视觉网奇2 小时前
FBX AnimSequence] 动画长度13与导入帧率30 fps(子帧0.94)不兼容。动画必须与帧边界对齐。
笔记·学习·ue5
weixin_395448912 小时前
main.c_cursor_0129
前端·网络·算法
2401_859049083 小时前
git submodule update --init --recursive无法拉取解决
前端·chrome·git
woodykissme3 小时前
倒圆角问题解决思路分享
笔记·学习·工艺
黎雁·泠崖3 小时前
Java核心基础API学习总结:从Object到包装类的核心知识体系
java·开发语言·学习
这是个栗子3 小时前
【Vue代码分析】前端动态路由传参与可选参数标记:实现“添加/查看”模式的灵活路由配置
前端·javascript·vue.js
刘一说3 小时前
Vue 动态路由参数丢失问题详解:为什么 `:id` 拿不到值?
前端·javascript·vue.js