你在电商网站登录后关掉页面,第二天打开,发现还是登录状态——网站凭什么记得你?你往购物车加了五件商品,中途去刷了会儿短视频,回来购物车还在——服务器又是靠什么把"你"和"你的购物车"绑在一起的?答案就是本文的主角:Cookie 与 Session。HTTP 本身"翻脸不认人",每次请求都是全新的,是这对搭档让网站有了"记忆"。读完本文,你会明白登录状态的完整链路,也能用 Python 亲手实现一套带登录的迷你网站。

1. 无状态的世界:为什么网站记不住你

HTTP 是无状态协议:服务器处理完一个请求就"失忆"了,它不记得这个请求来自谁,也不记得上一个请求是谁发的。无状态让服务器可以轻松扩展——任何一台机器都能处理任意请求,但也带来一个问题:怎么区分"你"和"别人"?购物车加进谁的名下?登录态怎么保持?

解决方案很朴素:给每个访客发一个"身份牌",访客每次来都把身份牌亮出来。这个身份牌就是 Cookie。下面这张表先给你一个整体印象,细节我们逐个展开:

维度CookieSession
存储位置浏览器(客户端)服务器
数据可见性用户可读可改用户不可见
容量单个约 4KB取决于服务器
扩展性天然支持多服务器多机部署要共享会话存储(如 Redis)
典型用途记住主题、埋点、购物车登录态、权限信息

2. Cookie 的工作方式:一次握手,长期有效

Cookie 的流程分三步:服务器在响应头里用 Set-Cookie 下发身份牌;浏览器收到后存起来;之后每次请求,浏览器自动在请求头 Cookie 里带上它。全程不需要 JavaScript 参与,浏览器自己搞定。

用 curl 亲眼看一下这个过程。第一步,请求一个会下发 Cookie 的接口,并把 Cookie 存进文件:

# -c 指定 cookie 文件,curl 会把 Set-Cookie 的内容写进去
curl -c cookies.txt "https://httpbin.org/cookies/set?name=codelab"

# 看看文件里存了什么
cat cookies.txt

cookie 文件是纯文本,每一行一个 Cookie,记录着域名、路径、过期时间和值。第二步,带上这个 Cookie 再访问,服务器就能"认出"你了:

# -b 让 curl 从文件读取 Cookie 并放进请求头
curl -b cookies.txt https://httpbin.org/cookies

返回的 JSON 里 cookies 字段会包含 name: codelab。注意 -c 是"存",-b 是"带",一存一带,正好模拟了浏览器的行为。想看细节,加 -v 参数,能看到响应里的 Set-Cookie 头和请求里的 Cookie 头:

curl -v -b "name=codelab" https://httpbin.org/cookies 2>&1 | grep -i cookie

输出里以 > 开头、写着 Cookie: name=codelab 的那一行,就是浏览器每次自动帮你干的事。curl 的 -v 输出中,> 前缀表示"发出去的请求头",< 前缀表示"收到的响应头"。如果 httpbin 偶尔抽风,拿第 4 节自己的服务器来试效果一样。

3. Cookie 的关键属性

Cookie 不是裸的 key-value,它带着一串属性,每个属性都对应一类安全问题:

其中 SameSite 值得多说两句:它有三个值,Strict 表示任何跨站请求都不带 Cookie,最安全但体验略差;Lax(现代浏览器的默认值)允许用户从外部链接点进来这种"顶级导航"携带 Cookie,日常够用;None 表示不限制,但必须配合 Secure 一起用,否则浏览器直接拒绝。在浏览器控制台里,你也可以用 JavaScript 读写 Cookie:

// 设置一个 1 小时后过期的主题 Cookie
document.cookie = "theme=dark; max-age=3600; path=/";
// 读取当前页面的所有 Cookie
console.log(document.cookie);

注意:通过 document.cookie 设置时,HttpOnly 的 Cookie 是读不到的——这正是它的作用:脚本拿不到,攻击者的 XSS 脚本也就拿不到。

4. 手写一个带登录的迷你网站

理论讲完,上代码。我们用 Python 标准库写一个迷你服务器:访问 /login 会下发登录 Cookie,再访问首页就能识别身份。这段代码一次跑完,自带验证:

import http.cookies
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import http.client

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        raw = self.headers.get("Cookie", "")
        cookie = http.cookies.SimpleCookie(raw)
        if self.path == "/login":
            # 下发会话 Cookie:1 小时后过期,并跳回首页
            self.send_response(302)
            self.send_header("Set-Cookie", "user=codelab; Path=/; Max-Age=3600")
            self.send_header("Location", "/")
            self.end_headers()
        elif self.path == "/":
            name = cookie.get("user")
            if name:
                body = ("欢迎回来," + name.value).encode()
            else:
                body = "未登录,请先访问 /login".encode()
            self.send_response(200)
            self.end_headers()
            self.wfile.write(body)
        else:
            self.send_response(404)
            self.end_headers()

    def log_message(self, *args):
        pass

server = HTTPServer(("127.0.0.1", 0), Handler)
port = server.server_address[1]
threading.Thread(target=server.serve_forever, daemon=True).start()

# 第一步:不带 Cookie 访问首页
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=3)
conn.request("GET", "/")
r = conn.getresponse()
print("未登录:", r.read().decode())
conn.close()

# 第二步:访问 /login,看 Set-Cookie 头
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=3)
conn.request("GET", "/login")
r = conn.getresponse()
print("登录响应:", r.status, "| Set-Cookie:", r.getheader("Set-Cookie"))
conn.close()

# 第三步:带上 Cookie 再访问首页
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=3)
conn.request("GET", "/", headers={"Cookie": "user=codelab"})
r = conn.getresponse()
print("带 Cookie:", r.read().decode())
conn.close()

server.shutdown()

输出依次是"未登录:请先访问 /login"、"登录响应:302 | Set-Cookie: user=codelab; Path=/; Max-Age=3600"、"带 Cookie:欢迎回来,codelab"。核心就三行:下发用 Set-Cookie,读取用 SimpleCookie 解析请求头,识别就是把 Cookie 里的值和自己的"数据库"比对。这里还有个细节:登录接口返回 302 并同时下发 Cookie,这是登录跳转的标准组合拳——浏览器收到响应后,先存 Cookie,再跟着 Location 跳转,之后每次请求都会带上这个 Cookie。

注意为了演示,用户名是写死的;真实系统里,user=codelab 应该换成一段随机字符串(会话 ID),而不是直接暴露用户名——这正是下一节的主题。

5. Session:把数据藏到服务端

Cookie 存在客户端,有个天然弱点:内容用户看得见、改得动。你把 user=admin 改成 user=root 试试?所以正经系统里,Cookie 只存一个随机 ID(Session ID),真正的用户数据放在服务器内存或数据库里,这个设计就叫 Session。

流程变成:登录成功 → 服务器生成随机 ID,把用户数据存进"会话表",再把 ID 通过 Set-Cookie 下发 → 之后每次请求,服务器用 ID 查会话表,取出用户数据。Cookie 和 Session 不是二选一,而是搭档:Cookie 是"钥匙",Session 是"保险柜"。钥匙丢了可以重配,但保险柜里绝不能放钥匙能直接打开的明文信息。

Session 也有自己的烦恼:会话表存在哪?如果只存在单台服务器的内存里,负载均衡把下一个请求转发到另一台机器,用户就"掉登录"了。常见的解法是共享会话存储(把会话表放进 Redis 之类的公共存储,所有服务器都去那里查)或者粘性会话(让同一个用户的请求总是打到同一台机器)。这些方案都可行,但都说明:状态在服务器上,扩展就得带着它一起想办法。

6. 安全提醒:CSRF 与 XSS

用 Cookie 做登录态,有两类攻击必须了解。XSS(跨站脚本)是攻击者在评论区、搜索框等入口注入脚本,脚本在你的浏览器里运行,偷走 Cookie 里的会话 ID;防御靠 HttpOnly 加输入转义。CSRF(跨站请求伪造)更阴险:你在银行网站登录着,又打开了一个恶意网页,那个网页里藏着一行 <img src="https://bank.com/transfer?to=attacker&amount=10000">,浏览器加载图片时会自动带上 bank.com 的 Cookie——请求就这么发出去了,而服务器根本分不清这是不是你本人点的。防御手段主要是 SameSite=Lax/Strict、校验请求来源(Origin/Referer)、以及在表单里塞一次性 token。

一句话总结:

💡 Cookie 负责"携带身份",Session 负责"保管身份"。凡是敏感数据一律放服务端;凡是下发给客户端的 Cookie,一律加上 HttpOnly + Secure + SameSite。

7. 总结与练习

本文讲清了登录态的完整链路:无状态协议 → Set-Cookie 下发 → 浏览器自动携带 → 服务器识别。Cookie 存客户端、Session 存服务端,两者配合才构成安全的会话管理。建议练习: