Cookies、Session、Token:一次性讲清状态保持(附实例+代码+项目实战)

109 阅读17分钟

Cookies、Session、Token:一次性讲清状态保持(附实例+代码+项目实战)

在Web开发中,"状态保持"是个绕不开的核心问题。你有没有想过:为什么登录一次后,网站就知道你是谁,能记住你的购物车和偏好设置?这背后其实就是Cookies、Session、Token这三大技术在默默发力。

很多开发者刚接触时总会混淆这三者:什么时候用Cookie?Session和Token的区别在哪?分布式系统为什么偏爱Token?今天这篇文章,从原理到代码实现,再到项目实战,一次性给你讲透!

一、为什么需要状态保持?

首先要明确一个关键前提:HTTP协议是无状态的。这意味着客户端(浏览器、APP)和服务器之间的每一次请求都是独立的,服务器不会主动记住上一次请求的任何信息。

举个例子:你第一次访问某电商网站,浏览商品后添加了几件到购物车;如果没有状态保持,当你刷新页面或再次发起请求时,服务器根本不知道"刚才是谁加了购物车",购物车数据就会丢失。

再比如登录功能:你输入账号密码验证通过后,下一次访问个人中心时,服务器必须能识别出"这个请求来自刚才登录成功的用户",否则就需要反复登录——这显然是无法接受的。

所以,状态保持的核心目标就是:让服务器在多次请求中,识别出同一个客户端,从而关联起该客户端的上下文信息(如登录状态、购物车数据、用户偏好等)。

核心痛点:HTTP无状态 → 服务器无法关联多次请求 → 需通过额外技术实现状态绑定

二、Cookie:浏览器时代的最初方案

Cookie是最早解决状态保持问题的技术,本质是服务器发送给客户端的一小段文本信息。客户端(主要是浏览器)会将其保存起来,之后每次向该服务器发起请求时,都会自动带上这段文本,让服务器识别身份。

类比理解:你去酒店入住时,前台给你一张房卡(Cookie),之后每次进出电梯、开门,只要出示房卡,酒店工作人员(服务器)就知道你是已入住的客人。

正面示例:服务器设置 Cookie

下面用Python Flask框架演示服务器如何设置Cookie,以及客户端如何自动携带。首先确保已安装Flask:pip install flask

服务端(Python Flask)

from flask import Flask, make_response, request

app = Flask(__name__)

# 1. 服务器设置Cookie的接口
@app.route('/set_cookie')
def set_cookie():
    # 创建响应对象
    resp = make_response("服务器已设置Cookie")
    # 设置Cookie:key为"user_id",value为"123456",max_age为3600秒(1小时有效期)
    resp.set_cookie(
        key="user_id",
        value="123456",
        max_age=3600,  # 有效期,单位秒
        path="/",       # 生效路径,/表示整个网站都生效
        httpOnly=True   # 关键!禁止前端JS读取,防止XSS攻击
    )
    return resp

# 2. 验证客户端是否携带Cookie的接口
@app.route('/get_cookie')
def get_cookie():
    # 从请求中获取Cookie
    user_id = request.cookies.get("user_id")
    if user_id:
        return f"服务器识别到你的身份:user_id={user_id}"
    else:
        return "未识别到身份,请先访问/set_cookie设置Cookie"

if __name__ == '__main__':
    app.run(debug=True)
    

测试流程

  1. 启动服务后,访问 http://127.0.0.1:5000/get_cookie,会提示"未识别到身份"
  2. 访问 http://127.0.0.1:5000/set_cookie,服务器返回"已设置Cookie"
  3. 再次访问 http://127.0.0.1:5000/get_cookie,会显示识别到的user_id

客户端请求会自动带上:打开浏览器开发者工具(F12)→ Network → 刷新/get_cookie页面,查看请求头中的Cookie字段,会发现"user_id=123456"已自动携带。

错误示例:把敏感信息写进 Cookie

Cookie是存储在客户端的文本,即使设置了httpOnly,也可能被用户通过浏览器设置查看或修改。如果直接将密码、银行卡号等敏感信息存入Cookie,会存在极大安全风险。

错误代码(禁止这样写)

@app.route('/set_sensitive_cookie')
def set_sensitive_cookie():
    resp = make_response("错误示例:设置敏感信息Cookie")
    # 严重错误:直接存储密码
    resp.set_cookie("password", "12345678", max_age=3600)
    return resp
    

调试技巧

  1. 浏览器查看Cookie:F12 → Application → Storage → Cookies,可查看当前网站的所有Cookie及属性
  2. 验证httpOnly有效性:在Console中输入 document.cookie,如果看不到httpOnly标记的Cookie,说明设置生效
  3. Postman测试:手动添加Cookie字段到请求头,模拟客户端携带场景

实际工作应用技巧

  • 设置合理有效期:临时登录场景用session(关闭浏览器失效),长期免登录用较长max_age(如7天)
  • 必加httpOnly和secure:httpOnly防XSS攻击,secure=True仅在HTTPS协议下传输Cookie
  • 存储非敏感信息:如用户ID、主题偏好、购物车临时标识等,避免敏感数据
  • 设置SameSite:SameSite=Lax或Strict,防止CSRF攻击(跨站请求伪造)

三、Session:把状态存回服务器

Cookie的痛点在于"状态存在客户端",存在被篡改风险。Session则反其道而行之:将核心状态数据存储在服务器,客户端只存一个唯一标识(SessionID)

类比理解:你去银行办理业务,工作人员给你一张叫号单(SessionID,存在Cookie里),你的个人信息、办理进度等核心数据都存在银行系统(服务器)里,你每次办理业务只需出示叫号单,工作人员通过单号调取你的数据。

核心流程:客户端首次请求 → 服务器创建Session(存储状态数据)→ 生成SessionID并通过Cookie发给客户端 → 后续请求客户端携带SessionID → 服务器通过ID查询Session数据。

正面示例

Flask内置了Session功能,默认基于Cookie存储SessionID,下面演示登录状态保持场景:

服务端(Python Flask)

from flask import Flask, session, request, redirect, url_for

app = Flask(__name__)
# 关键:配置Session密钥(用于加密SessionID,必须设置,生产环境要复杂且保密)
app.secret_key = "dev_secret_key_123456"  # 生产环境用os.urandom(24)生成随机密钥

# 模拟用户数据库
USER_DB = {
    "admin": "admin123",
    "user1": "user123"
}

# 登录接口:验证成功后创建Session
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        # 验证账号密码
        if USER_DB.get(username) == password:
            # 创建Session,存储用户信息(服务器端)
            session['username'] = username
            session['is_login'] = True
            return redirect(url_for('index'))
        else:
            return "账号或密码错误"
    # GET请求返回登录表单
    return '''
        <form method="post">
            用户名:<input type="text" name="username"><br>
            密码:<input type="password" name="password"><br>
            <input type="submit" value="登录">
        </form>
    '''

# 首页:需要登录才能访问
@app.route('/')
def index():
    # 验证Session状态
    if session.get('is_login'):
        return f"欢迎回来,{session['username']}!<a href='/logout'>退出登录</a>"
    else:
        return "请先<a href='/login'>登录</a>"

# 退出登录:删除Session
@app.route('/logout')
def logout():
    # 清除Session中的登录状态
    session.pop('username', None)
    session.pop('is_login', None)
    return "退出成功,<a href='/login'>重新登录</a>"

if __name__ == '__main__':
    app.run(debug=True)
    

测试说明

  1. 访问 http://127.0.0.1:5000,会被引导到登录页
  2. 输入正确账号(如admin)和密码(admin123),登录后进入首页
  3. 查看浏览器Cookie,会发现有一个名为"session"的字段(存储的是加密后的SessionID)
  4. 退出登录后,Session中的状态被清除,再次访问首页需要重新登录

错误示例:把 Session 存在本地文件

Flask默认将Session数据存储在服务器的本地文件中(开发环境),这种方式在生产环境存在严重问题:

  • 分布式部署时,多台服务器之间的Session不共享,导致用户在A服务器登录后,访问B服务器仍需重新登录
  • 本地文件读写效率低,高并发场景会成为瓶颈

错误配置(默认开发环境,生产禁用)

# Flask默认Session存储方式(本地文件),无需手动配置,但生产环境必须修改
app.config['SESSION_TYPE'] = 'filesystem'  # 默认值,生产禁用
    

正确做法:生产环境将Session存储在Redis、Memcached等分布式缓存中,确保多服务器共享。

调试技巧

  1. 查看SessionID:浏览器Cookie中的"session"字段就是加密后的SessionID
  2. 调试Session数据:开发环境可打印session对象,如 print(session),查看存储的状态
  3. Redis存储调试:如果用Redis存储,可通过 redis-cli keys "*session*" 查看Session数据

实际工作应用技巧

  • Session存储介质选择:开发环境用文件,测试/生产用Redis(高性能、支持分布式)
  • 设置Session有效期:避免Session长期有效占用资源,如配置 PERMANENT_SESSION_LIFETIME = timedelta(hours=2)
  • 敏感数据加密:即使存在服务器,用户密码等敏感信息也需加密后再存入Session
  • 防止Session固定攻击:用户登录成功后,重新生成一个新的SessionID(Flask已默认实现)

四、Token:适配移动端与分布式时代

Session虽然解决了Cookie的安全问题,但在移动端(APP、小程序)和分布式架构下仍有局限:移动端不一定支持Cookie,分布式环境需要共享Session存储(如Redis)。

Token应运而生:服务器生成一串加密的字符串(Token),客户端存储(如浏览器localStorage、APP本地缓存),每次请求时主动在请求头中携带。服务器通过解密Token验证身份,无需存储Token本身(如JWT)。

核心优势:无Cookie依赖、服务器无状态(无需存储Token)、天然支持分布式和移动端。

JWT 的基本结构(面试必问)

目前最流行的Token标准是JWT(JSON Web Token),本质是一串由点(.)分隔的字符串,分为三部分:头部(Header)、载荷(Payload)、签名(Signature)

# JWT示例(三部分用.分隔)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNzE5NzQwMDAwLCJleHAiOjE3MTk3NDM2MDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  1. Header(头部) :指定加密算法和Token类型,Base64编码(可解密) { `` "alg": "HS256", // 加密算法(HMAC SHA256) `` "typ": "JWT" // Token类型 ``}
  2. Payload(载荷) :存储核心信息(如用户ID、过期时间),Base64编码(可解密,不要存敏感信息 { `` "username": "admin", // 自定义信息(用户标识) `` "iat": 1719740000, // 签发时间(时间戳) `` "exp": 1719743600 // 过期时间(时间戳,这里是1小时) ``}
  3. Signature(签名) :用Header指定的算法,结合服务器密钥对Header和Payload的编码结果进行加密,用于验证Token是否被篡改。签名无效则Token失效。

关键提醒:Payload是Base64编码(不是加密),任何人都能解密查看,所以绝对不能存密码、手机号等敏感信息!

正面示例:后端生成 Token

使用PyJWT库生成和验证JWT,先安装依赖:pip install pyjwt

服务端(Python Flask)

from flask import Flask, request, jsonify
import jwt
import time
from datetime import datetime, timedelta

app = Flask(__name__)
# 关键:JWT密钥(生产环境必须保密,建议用环境变量存储)
SECRET_KEY = "dev_jwt_secret_key_123456"  # 生产环境:os.getenv("JWT_SECRET")

# 模拟用户数据库
USER_DB = {
    "admin": "admin123",
    "user1": "user123"
}

# 1. 登录接口:验证成功后生成JWT
@app.route('/login_jwt', methods=['POST'])
def login_jwt():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    
    # 验证账号密码
    if USER_DB.get(username) != password:
        return jsonify({"code": 401, "msg": "账号或密码错误"})
    
    # 生成JWT(设置过期时间1小时)
    payload = {
        "username": username,
        "iat": datetime.utcnow(),  # 签发时间(UTC时间)
        "exp": datetime.utcnow() + timedelta(hours=1)  # 过期时间
    }
    # 加密生成Token
    token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
    
    return jsonify({
        "code": 200,
        "msg": "登录成功",
        "data": {"token": token}
    })

# 2. 需验证Token的接口(示例:获取用户信息)
@app.route('/get_user_info')
def get_user_info():
    # 从请求头获取Token(约定格式:Authorization: Bearer <token>)
    auth_header = request.headers.get('Authorization')
    if not auth_header or not auth_header.startswith('Bearer '):
        return jsonify({"code": 403, "msg": "请携带有效的Token"})
    
    # 提取Token
    token = auth_header.split(' ')[1]
    try:
        # 解密Token(自动验证过期时间和签名)
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        username = payload.get('username')
        return jsonify({
            "code": 200,
            "msg": "成功",
            "data": {"username": username, "role": "admin" if username == "admin" else "user"}
        })
    except jwt.ExpiredSignatureError:
        return jsonify({"code": 403, "msg": "Token已过期,请重新登录"})
    except jwt.InvalidTokenError:
        return jsonify({"code": 403, "msg": "Token无效或已被篡改"})

if __name__ == '__main__':
    app.run(debug=True)
    

测试流程(用Postman)

  1. 发送POST请求到 http://127.0.0.1:5000/login_jwt,请求体JSON为: { `` "username": "admin", `` "password": "admin123" ``} 成功后会返回token
  2. 发送GET请求到 http://127.0.0.1:5000/get_user_info,在请求头添加: Authorization: Bearer 刚才返回的token 成功后会返回用户信息
  3. 如果Token过期或篡改,会返回403错误

错误示例:JWT 存过多敏感信息

很多开发者误以为JWT是加密的,就把敏感信息存进Payload,这是严重错误。因为Payload是Base64编码,可直接解密查看。

错误代码(禁止这样写)

# 严重错误:Payload中存储密码
payload = {
    "username": "admin",
    "password": "admin123",  # 绝对禁止!
    "exp": datetime.utcnow() + timedelta(hours=1)
}
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
# 任何人拿到Token后,用Base64解密Payload就能看到密码!
    

调试技巧

  1. 在线解析JWT:访问 jwt.io/,粘贴Token可直接查看Header和Payload(验证是否泄露敏感信息)
  2. Token过期调试:开发时可将过期时间设短(如30秒),测试过期逻辑
  3. 请求头携带检查:F12→Network→查看请求头,确认Authorization字段是否正确携带

实际工作应用技巧

  • 密钥管理:生产环境JWT密钥必须保密,通过环境变量或配置中心存储,避免硬编码
  • 过期时间设计:采用"短Token+刷新Token"机制,如访问Token有效期2小时,刷新Token有效期7天,避免频繁登录
  • 客户端存储:浏览器存localStorage(方便获取)或sessionStorage(关闭页面失效),APP存本地沙盒存储
  • Token注销:由于服务器不存储Token,注销需客户端删除本地Token,或在服务器维护"黑名单"(如Redis存储已注销Token,有效期同Token过期时间)

五、项目实战:构建一个支持三种认证方式的简易系统

下面整合前面的知识,构建一个支持Cookie、Session、Token三种认证方式的用户中心系统,实现"登录-获取信息-退出"完整流程。

实战功能需求

  1. 提供三种登录接口:/login_cookie、/login_session、/login_token
  2. 提供统一的用户信息接口:/user_info(自动识别认证方式)
  3. 提供三种退出接口:/logout_cookie、/logout_session、/logout_token

样例代码:完整系统实现

from flask import Flask, request, jsonify, make_response, session, redirect, url_for
import jwt
from datetime import datetime, timedelta
import os

app = Flask(__name__)
# 配置
app.secret_key = os.urandom(24)  # Session密钥(随机生成)
JWT_SECRET = os.getenv("JWT_SECRET", "prod_secret_123")  # JWT密钥
USER_DB = {"admin": "admin123", "user1": "user123"}  # 模拟数据库

# -------------------------- 公共工具函数 --------------------------
def verify_user(username, password):
    """验证账号密码"""
    return USER_DB.get(username) == password

def get_user_info(username):
    """获取用户信息(模拟从数据库查询)"""
    return {
        "username": username,
        "role": "admin" if username == "admin" else "user",
        "avatar": "https://example.com/avatar.jpg"
    }

# -------------------------- Cookie认证 --------------------------
@app.route('/login_cookie', methods=['POST'])
def login_cookie():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    if not verify_user(username, password):
        return jsonify({"code": 401, "msg": "账号密码错误"})
    # 设置Cookie
    resp = jsonify({"code": 200, "msg": "Cookie登录成功"})
    resp.set_cookie("username", username, max_age=3600, httpOnly=True, secure=False)
    return resp

@app.route('/logout_cookie')
def logout_cookie():
    # 删除Cookie
    resp = jsonify({"code": 200, "msg": "Cookie退出成功"})
    resp.delete_cookie("username")
    return resp

# -------------------------- Session认证 --------------------------
@app.route('/login_session', methods=['POST'])
def login_session():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    if not verify_user(username, password):
        return jsonify({"code": 401, "msg": "账号密码错误"})
    # 设置Session
    session['username'] = username
    session['is_login'] = True
    return jsonify({"code": 200, "msg": "Session登录成功"})

@app.route('/logout_session')
def logout_session():
    # 清除Session
    session.pop('username', None)
    session.pop('is_login', None)
    return jsonify({"code": 200, "msg": "Session退出成功"})

# -------------------------- Token认证 --------------------------
@app.route('/login_token', methods=['POST'])
def login_token():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    if not verify_user(username, password):
        return jsonify({"code": 401, "msg": "账号密码错误"})
    # 生成JWT
    payload = {
        "username": username,
        "iat": datetime.utcnow(),
        "exp": datetime.utcnow() + timedelta(hours=1)
    }
    token = jwt.encode(payload, JWT_SECRET, algorithm="HS256")
    return jsonify({"code": 200, "msg": "Token登录成功", "data": {"token": token}})

# -------------------------- 统一用户信息接口 --------------------------
@app.route('/user_info')
def user_info():
    # 1. 尝试Token认证
    auth_header = request.headers.get('Authorization')
    if auth_header and auth_header.startswith('Bearer '):
        try:
            token = auth_header.split(' ')[1]
            payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
            username = payload.get('username')
            return jsonify({"code": 200, "data": get_user_info(username), "auth_type": "token"})
        except:
            pass  # Token无效,尝试其他方式
    
    # 2. 尝试Session认证
    if session.get('is_login'):
        username = session.get('username')
        return jsonify({"code": 200, "data": get_user_info(username), "auth_type": "session"})
    
    # 3. 尝试Cookie认证
    username = request.cookies.get('username')
    if username and username in USER_DB:
        return jsonify({"code": 200, "data": get_user_info(username), "auth_type": "cookie"})
    
    # 所有认证方式失败
    return jsonify({"code": 403, "msg": "未登录,请先通过任意方式登录"})

if __name__ == '__main__':
    app.run(debug=True)
    

测试验证

  • 用Postman通过Token登录后,访问/user_info,返回auth_type: "token"
  • 通过Cookie登录后,访问/user_info,返回auth_type: "cookie"
  • 退出后再次访问/user_info,返回403错误

六、高级技巧:如何选择适合自己的认证方式?

三种方式没有绝对的优劣,关键看业务场景。下面从6个维度给出选择建议:

场景维度CookieSessionToken(JWT)
适用客户端仅浏览器(支持Cookie)主要浏览器(依赖Cookie存SessionID)浏览器、APP、小程序、第三方接口(无Cookie依赖)
分布式架构支持(但需注意跨域Cookie问题)需共享存储(如Redis),略复杂天然支持(服务器无状态),推荐
安全性较低(客户端可修改,需httpOnly防护)较高(状态存服务器,仅传SessionID)中高(签名防篡改,但Payload可解密)
开发成本低(浏览器自动携带)中(需配置存储介质)中(需手动处理Token携带和验证)
注销难度易(服务器删除Cookie)易(服务器删除Session)较难(需客户端删除或维护黑名单)
推荐场景简单网站、非敏感信息存储(如主题偏好)传统Web网站、需要强状态管理(如电商购物车)移动端APP、分布式系统、第三方API接口

实战建议:目前主流方案是"Token为主,Session为辅"——移动端和分布式服务用Token,传统Web后台用Session,简单场景用Cookie存储非敏感信息。

七、拓展原理:为什么分布式架构更偏爱 Token?

分布式架构的核心是"多台服务器协同工作,对外提供统一服务",这就对状态保持提出了"跨服务器共享状态"的要求。Session和Token在这一点上的差异是关键:

  1. Session的瓶颈:状态依赖存储Session的核心状态数据存在服务器端,如果用本地文件存储,多台服务器之间的Session无法共享——用户在服务器A登录后,请求被负载均衡分发到服务器B,B上没有该用户的Session,就会要求重新登录。虽然可以通过Redis等分布式缓存解决Session共享问题,但会带来额外的架构复杂度:需要维护Redis集群、处理缓存穿透/击穿问题、增加网络开销(服务器每次验证都要查Redis)。
  2. Token的优势:服务器无状态Token(如JWT)的核心是"服务器无需存储Token信息"——Token本身包含了用户标识和过期时间,服务器只需通过密钥解密和验证签名,就能确认用户身份。这种无状态特性完美适配分布式架构:无需共享存储,减少架构复杂度
  3. 多台服务器可独立验证Token,无需通信,提高并发性能
  4. 支持水平扩展,新增服务器无需额外配置

简单说:分布式架构追求"去中心化",而Token的无状态特性正好契合这一理念,Session则需要"中心化存储"作为支撑,因此Token更受青睐。

八、总结

回到最初的问题:Cookies、Session、Token到底是什么关系?其实它们都是为了解决HTTP无状态问题的技术方案,只是从不同角度实现:

  • Cookie:客户端存储状态,简单但安全性低,依赖浏览器
  • Session:服务器存储状态,安全性高但依赖共享存储,适配传统Web
  • Token:客户端存储加密令牌,服务器无状态,适配移动端和分布式

最后用一张图总结核心区别和适用场景,方便大家收藏记忆:

image.png

关键takeaway:没有最好的方案,只有最适合的方案。根据客户端类型、架构模式、安全要求选择认证方式,才是高效开发的核心。

如果这篇文章帮你理清了三者的关系,欢迎点赞收藏~ 有问题或不同见解,评论区见!