用户登录后,服务器怎么知道「你是谁」?Cookie / Session / JWT 全解析

2024-07-09 编程开发

当你登录一个网站后,刷新页面不会要求重新登录,换个页面也还是登录状态。服务器明明是无状态的,它怎么知道「刚才那个人又来了」?

这就是 Web 认证体系要解决的核心问题。本文带你从 HTTP 无状态的本质出发,逐步理解 Cookie、Session、JWT 三种主流认证方案的原理、差异和最佳实践。


1. 根源:HTTP 的无状态性

HTTP 协议本身是无状态的——每个请求都是独立的,服务器不会记住「这个请求是刚才那个人发的」。

请求1: GET /api/profile
          ↓ 服务器:你是谁?我不知道。

请求2: GET /api/orders
          ↓ 服务器:你还是谁?我还是不知道。

但 Web 应用需要状态:登录、购物车、浏览历史……于是,Cookie 诞生了。


2. Cookie:在浏览器里「贴标签」

2.1 工作原理

Cookie 是服务器让浏览器帮忙存储的一小段数据(通常 4KB 以内)。浏览器在每次请求时自动携带。

┌────────┐                              ┌────────┐
│ 浏览器  │  ─── GET /api/profile ────→ │ 服务器  │
│        │  ←── 200 + Set-Cookie:       │        │
│        │       user_id=abc123 ──────  │        │
│        │                              │        │
│ 浏览器  │  ─── GET /api/orders ─────→ │ 服务器  │
│ (自动   │     Cookie: user_id=abc123  │ (读取   │
│  带上)  │  ←── 200 [订单数据] ──────  │  Cookie)│
└────────┘                              └────────┘
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax; Max-Age=3600; Path=/
属性 说明 安全影响
HttpOnly 禁止 JavaScript 访问 防止 XSS 窃取 Cookie
Secure 仅 HTTPS 传输 防止中间人拦截
SameSite 跨站请求是否携带 防止 CSRF 攻击
Max-Age 过期时间(秒) 持久化 vs 会话
Domain Cookie 生效域名 控制作用范围
Path Cookie 生效路径 精细控制

Node.js (Express):

// 登录成功,设置 Cookie
res.cookie('user_id', 'abc123', {
  httpOnly: true,
  secure: true,          // 生产环境使用 HTTPS
  sameSite: 'strict',
  maxAge: 24 * 60 * 60 * 1000,  // 24 小时
});

// 读取 Cookie
app.get('/api/profile', (req, res) => {
  const userId = req.cookies.user_id;
  // 根据 userId 查询用户数据...
});

Python (Flask):

from flask import Flask, request, make_response

@app.route('/login', methods=['POST'])
def login():
    # 验证用户名密码...
    resp = make_response({'status': 'ok'})
    resp.set_cookie(
        'user_id', 'abc123',
        httponly=True,
        secure=True,
        samesite='Strict',
        max_age=86400
    )
    return resp

@app.route('/api/profile')
def profile():
    user_id = request.cookies.get('user_id')
    # 查询用户信息...

3. Session:把数据存在服务端

  • Cookie 大小限制(4KB),存不下复杂数据
  • Cookie 存储在客户端,可以被篡改
  • 敏感信息不应放在客户端

因此,Session 机制诞生:Cookie 只存一个 Session ID,真正的数据存在服务端。

3.2 工作流程

┌────────┐                    ┌────────┐     ┌──────────┐
│ 浏览器  │                    │ 服务器  │     │ 存储层   │
│        │                    │        │     │(Redis/DB)│
└───┬────┘                    └───┬────┘     └────┬─────┘
    │                             │               │
    │  POST /login                │               │
    │  {user, password}           │               │
    │────────────────────────────→│               │
    │                             │ 验证密码      │
    │                             │ 生成 session  │
    │                             │──────────────→│ set session:xyz
    │                             │               │  {user_id, role}
    │  ← 200                      │               │
    │  Set-Cookie:                │               │
    │  session_id=xyz             │               │
    │                             │               │
    │  GET /api/profile           │               │
    │  Cookie: session_id=xyz     │               │
    │────────────────────────────→│               │
    │                             │ 查询 session  │
    │                             │──────────────→│ get session:xyz
    │                             │←──────────────│ {user_id, role}
    │  ← 200 【用户数据】          │               │

3.3 Node.js 实现(express-session + Redis)

3.4 Python 实现(Flask-Session + Redis)


4. JWT:把「身份证」交给用户

4.1 JWT 是什么?

JWT(JSON Web Token)是一种自包含的令牌,用户数据直接编码在 Token 中,服务端不需要存储任何会话信息。

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6ImFkbWluIn0.abc123def456
│        Header       │           Payload            │     Signature     │
│   Base64 编码的     │   Base64 编码的用户数据      │   防篡改签名      │
│   算法信息          │                               │                   │

4.2 工作流程

┌────────┐                              ┌────────┐
│ 客户端  │                              │ 服务器  │
│        │  POST /login                 │        │
│        │  {user, password}            │        │
│        │─────────────────────────────→│        │
│        │                              │ 验证   │
│        │  ← 200 { token: "eyJ..." }  │ 签发 JWT│
│        │                              │        │
│ 客户端  │                              │        │
│ 存储    │  GET /api/profile            │        │
│ JWT 到  │  Authorization: Bearer eyJ..│        │
│localStor│─────────────────────────────→│ 校验签名│
│ age     │                              │ 读取 payload│
│        │  ← 200 【用户数据】            │        │
└────────┘                              └────────┘

4.3 Node.js 实现

4.4 Python 实现(PyJWT)


5. 三种方案全面对比

维度 Cookie(明文) Session JWT
存储位置 客户端 服务端 客户端
状态 有状态 有状态 无状态
扩展性 差(需共享存储) 差(需共享存储) 好(天然支持)
安全性 低(可篡改) 高(服务端可信) 高(签名防篡改)
性能 高 中(每次查存储) 高(本地校验)
注销控制 即时 即时 困难(需黑名单)
移动端友好 一般 一般 好(Header 传递)
跨域 受同源策略限制 受同源策略限制 天然支持
数据大小 极有限(4KB) 不限制 有限(Header 限制)
适用场景 简单状态标记 传统 Web 应用 API / 微服务 / SPA

6. 安全注意事项

6.1 XSS(跨站脚本攻击)

攻击者注入恶意脚本 → 盗取 Cookie / localStorage 中的 Token

防御:

// ✅ 对 Session Cookie 设置 HttpOnly(JS 无法读取)
res.cookie('session_id', value, { httpOnly: true });

// ⚠️ JWT 存在 localStorage 中可以被 XSS 读取
// 缓解:将 JWT 也存为 HttpOnly Cookie,或用 refresh token 机制

6.2 CSRF(跨站请求伪造)

受害者访问恶意网站 → 恶意网站自动提交请求 → 浏览器自动带上 Cookie

防御:

# Flask 示例:CSRF Token
from flask_wtf.csrf import CSRFProtect

csrf = CSRFProtect(app)

# 前端请求时带上 CSRF Token
# 或使用 SameSite Cookie 属性
@app.after_request
def set_cookie(response):
    response.set_cookie('session_id', value, samesite='Strict')
    return response

6.3 JWT 特殊注意事项

// ❌ 不要把敏感信息放在 payload 中
// payload 只是 Base64 编码,不是加密!
jwt.sign({
  userId: 123,
  password: 'secret123'  // ❌ 任何人都能解码看到!
}, SECRET_KEY);

// ✅ 只放非敏感的标识信息
jwt.sign({
  userId: 123,
  role: 'user'
}, SECRET_KEY);

// ✅ 使用较短的过期时间
jwt.sign(payload, SECRET_KEY, { expiresIn: '15m' });  // access token

// ✅ 配合 refresh token 实现长期登录
// access token: 15 分钟过期
// refresh token: 7 天过期,存在 HttpOnly Cookie 中

6.4 安全最佳实践清单

措施 Cookie/Session JWT
使用 HTTPS ✅ 必须 ✅ 必须
HttpOnly Cookie ✅ 推荐 ✅ 推荐(存 token 时)
Secure Cookie ✅ 必须 ✅ 必须
SameSite Cookie ✅ Strict/Lax ✅ Strict/Lax
限制过期时间 ✅ ✅ Access Token 短(15min)
服务端登出黑名单 N/A 可选
密码哈希存储 ✅ bcrypt ✅ bcrypt
速率限制 ✅ 登录接口 ✅ 登录接口

7. 选型指南

你的应用架构是什么样的?

├── 传统服务端渲染 (SSR)
│   └── 用 Session + Cookie(成熟、安全)
│
├── SPA + REST API(同一域名)
│   └── 两者皆可
│       简单场景 → Session + Cookie
│       需要扩展 → JWT + HttpOnly Cookie
│
├── SPA + API(跨域)
│   └── JWT(Authorization Header)
│
├── 微服务架构
│   └── JWT(无状态,服务间无需查询)
│
├── 移动端 App
│   └── JWT(不依赖 Cookie)
│
└── 需要第三方集成 / OAuth2
    └── JWT(标准格式,广泛支持)

8. 小结

三种方案各有优劣,没有银弹,关键看场景:

  1. Cookie:最简单,但功能有限,适合少量非敏感数据
  2. Session:传统 Web 应用首选,安全可控,但需要服务端存储
  3. JWT:现代 API / 微服务首选,无状态、易扩展,但要处理好过期和注销

无论选哪种方案,安全三件套不能忘:HTTPS + HttpOnly + SameSite。

认证是 Web 安全的基石,花时间把这一块吃透,受益终身。