第6章 6.3 鉴权:JWT 与 Session
🎯 开场 3 分钟:为什么你每次登录都要重新输入密码?
你有没有遇到过这种情况:在网上看中一件衣服,加了购物车,关了浏览器第二天再打开——咦,居然还在!购物车里的东西没丢,网站好像"记住"你了。
但仔细想想,网站是怎么记住你的?
HTTP 有个特点,叫「无状态」——每次请求之间,服务器根本不记得你是谁。你打开淘宝,服务器问:「你是谁?」你输入账号密码登录,服务器说:「哦,你是小明,欢迎!」但下一秒你点开购物车,服务器又问:「你是谁?」——它又忘了你刚才登录过。
这就是鉴权要解决的核心问题:怎么让服务器在多次请求之间「记住」你是谁。
上一章我们学了 HTTPS,知道数据在传输过程中是加密的,别人偷不到。但光有加密还不够,你还得证明「我是我」,这就是鉴权(Authentication)的职责。
本章学完,你能:
- 理解 Session 和 JWT 两种主流鉴权方式的区别
- 用 Python 亲手实现一个带登录功能的 Web 接口
- 知道什么时候该用哪种方式
🧱 基础 25 分钟:Session 和 JWT 到底是个啥?
先说个生活比喻,帮你建立直观感觉。
6.3.1 Session:酒店房卡模式
类比时间⌚
你去酒店住宿,身份证登记后,前台给你一张房卡。你拿着房卡进电梯、开门、回房间——全程只需要这一张卡。退房时卡就失效了。
Session 的原理一模一样:
- 你登录网站,输入账号密码
- 服务器验证通过,在服务器端创建一条记录(Session)
- 服务器给你一张「房卡」(叫 Session ID,是一串随机字符)
- 你的浏览器把这张「房卡」存起来(Cookie)
- 以后你访问任何页面,浏览器自动把「房卡」递给服务器
- 服务器看到「房卡」,就知道「哦,这是小明」
为什么要这么设计?
因为服务器要在多次请求之间认出你,得有个地方存你的信息。Session 就存在服务器内存(或数据库)里。
来,看代码:
# 安装依赖:pip install flask flask-session
from flask import Flask, session, request, jsonify
from flask_session import Session
app = Flask(__name__)
app.secret_key = "小明的秘密钥匙" # 用来加密 session 数据的密钥
app.config["SESSION_TYPE"] = "filesystem" # session 存到文件系统
Session(app)
@app.route("/登录", methods=["POST"])
def 登录():
数据 = request.get_json()
用户名 = 数据.get("用户名")
密码 = 数据.get("密码")
# 假设这里有真正的数据库验证
if 用户名 == "小明" and 密码 == "123456":
session["用户"] = 用户名 # 创建 session
return jsonify({"消息": "登录成功", "用户名": 用户名})
return jsonify({"消息": "账号或密码错误"}), 401
@app.route("/个人中心")
def 个人中心():
if "用户" in session:
return jsonify({"消息": f"欢迎回来,{session['用户']}!"})
return jsonify({"消息": "请先登录"}), 401
@app.route("/登出", methods=["POST"])
def 登出():
session.pop("用户", None) # 删除 session
return jsonify({"消息": "已退出登录"})
if __name__ == "__main__":
app.run(port=5000, debug=True)
运行结果:
# 1. 先登录
curl -X POST http://localhost:5000/登录 \
-H "Content-Type: application/json" \
-d '{"用户名":"小明","密码":"123456"}'
# 返回:{"消息": "登录成功", "用户名": "小明"}
# 2. 访问个人中心(不带 session)
curl http://localhost:5000/个人中心
# 返回:{"消息": "请先登录"}
# 3. 再次登录后再访问个人中心
# (第二次请求时浏览器会自动带上 cookie 中的 session id)
# 返回:{"消息": "欢迎回来,小明!"}
这行在干嘛:
- session["用户"] = 用户名:把用户名存进 session,类似往房卡里写数据
- session.pop("用户", None):销毁 session,类似退房

6.3.2 JWT:自带身份证的模式
类比时间⌚
你去游乐园,买了一张通票。票上直接印着你的名字、照片、有效日期。你拿着这张票,一天内随便进出哪个项目,检票员扫一眼就知道「这是小明买的票,还没过期,可以进」。
不需要游乐园前台记住你是谁——所有信息都在票上写着。
JWT(JSON Web Token)就是这个原理:
- 你登录网站,输入账号密码
- 服务器验证通过,生成一个「通票」(JWT)
- 这个「通票」里包含你的信息(用户名、过期时间等),并且服务器用私钥签名防止伪造
- 服务器把「通票」发给你
- 你的浏览器存起来(通常存 localStorage 或 cookie)
- 以后你访问任何页面,请求头里带着这张「通票」
- 服务器验证签名就知道「这票是真的,是小明的,没过期」
和 Session 的核心区别:
| Session | JWT | |
|---|---|---|
| 存储位置 | 服务器端 | 客户端(浏览器) |
| 状态 | 有状态(服务器要存数据) | 无状态(服务器不存数据) |
| 扩展性 | 多服务器集群时要共享 session | 天生适合分布式 |
| 安全性 | 泄露了 session id 别人就能用 | 泄露了 token 别人就能用(但有过期时间) |
来,看代码:
# 安装依赖:pip install pyjwt
import jwt
import datetime
# 这是服务器的私钥,只有服务器知道
秘钥 = "小明的超级秘密钥匙"
def 生成Token(用户名):
"""登录成功后,生成 JWT"""
过期时间 = datetime.datetime.utcnow() + datetime.timedelta(hours=24) # 24小时后过期
载荷 = {
"用户名": 用户名,
"exp": 过期时间 # expiration time
}
# jwt.encode(载荷, 秘钥, 算法)
token = jwt.encode(载荷, 秘钥, algorithm="HS256")
return token
def 验证Token(token):
"""验证 JWT 是否有效"""
try:
载荷 = jwt.decode(token, 秘钥, algorithms=["HS256"])
return {"有效": True, "用户名": 载荷.get("用户名")}
except jwt.ExpiredSignatureError:
return {"有效": False, "原因": "Token已过期"}
except jwt.InvalidTokenError:
return {"有效": False, "原因": "Token无效"}
# 模拟登录
token = 生成Token("小明")
print(f"生成的Token: {token}")
# 模拟验证
结果 = 验证Token(token)
print(f"验证结果: {结果}")
运行结果:
生成的Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IuW8o-ixNIiwiZXhwIjoxNzE2MjM0ODAwfQ.7d2v1kHqQJZxY8gR3hQJ6mN9x1yT5wE4cD8sL2pO9kM
验证结果: {'有效': True, '用户名': '小明'}
这行在干嘛:
- jwt.encode(载荷, 秘钥, algorithm="HS256"):用私钥签名,生成 Token
- jwt.decode(token, 秘钥, algorithms=["HS256"]):验证签名和解码
Token 长啥样?
JWT 的格式是 xxxxx.yyyyy.zzzzz,由三部分组成:
- xxxxx(Header):告诉别人这是什么算法生成的
- yyyyy(Payload):存的用户信息
- zzzzz(Signature):签名,确保没人篡改过
你可以在 https://jwt.io/ 这个网站在线解码看看。

6.3.3 什么时候用哪种?
用 Session 的场景:
- 小型网站,用户量不大
- 需要强制让某个用户下线(删掉服务端 session 就行)
- Token 不想泄露在 URL 里(Session 存在 Cookie 中更安全)
用 JWT 的场景:
- 分布式系统,多个服务器要共享认证状态
- 移动端 App(移动端不太方便用 Cookie)
- 想让某个接口可以被第三方调用(OpenID Connect 就是基于 JWT 的)
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):Flask 实现带 Session 的登录
先来个简单的,用 Flask 实现最基础的登录功能。
# 文件名: login_session.py
# 运行: python login_session.py
from flask import Flask, session, request, jsonify
app = Flask(__name__)
app.secret_key = "very_secret_key_123" # 换个复杂的密钥
# 模拟数据库:用户名 -> 密码
用户数据库 = {
"小明": "password123",
"小红": "ILovePython",
"老王": "admin888"
}
@app.route("/login", methods=["POST"])
def login():
data = request.get_json()
username = data.get("username")
password = data.get("password")
if username in 用户数据库 and 用户数据库[username] == password:
session["user"] = username
return jsonify({"success": True, "message": f"欢迎 {username}!"})
return jsonify({"success": False, "message": "账号或密码错误"}), 401
@app.route("/profile")
def profile():
if "user" in session:
return jsonify({"success": True, "user": session["user"]})
return jsonify({"success": False, "message": "请先登录"}), 401
@app.route("/logout", methods=["POST"])
def logout():
session.pop("user", None)
return jsonify({"success": True, "message": "已退出"})
if __name__ == "__main__":
print("登录接口: POST /login 参数: username, password")
print("个人中心: GET /profile")
print("退出登录: POST /logout")
app.run(port=5000, debug=True)
测试方法:
# 1. 登录
curl -c cookies.txt -X POST http://localhost:5000/login \
-H "Content-Type: application/json" \
-d '{"username":"小明","password":"password123"}'
# 2. 带着 cookie 访问个人中心
curl -b cookies.txt http://localhost:5000/profile
# 3. 退出
curl -b cookies.txt -X POST http://localhost:5000/logout
预期输出:
# 登录
{"success": true, "message": "欢迎 小明!"}
# 访问个人中心
{"success": true, "user": "小明"}
# 退出后再访问
{"success": false, "message": "请先登录"}
一句话解释: Session 像房卡,服务器帮你记着你是谁,你只需要带着卡就行。
项目 2(15 分钟):Flask + JWT 实现带过期时间的 API 鉴权
这回换 JWT,模拟一个任务管理 API。
# 文件名: task_api_jwt.py
# 运行: python task_api_jwt.py
from flask import Flask, request, jsonify
import jwt
import datetime
app = Flask(__name__)
秘钥 = "jwt_super_secret_key_456"
# 模拟数据库
用户库 = {"小明": "123", "小红": "456"}
任务库 = {
"小明": [{"id": 1, "内容": "写周报", "完成": False}],
"小红": [{"id": 2, "内容": "开会", "完成": True}]
}
def 生成Token(username):
payload = {
"username": username,
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=2) # 2小时过期
}
return jwt.encode(payload, 秘钥, algorithm="HS256")
def 验证Token(token):
try:
payload = jwt.decode(token, 秘钥, algorithms=["HS256"])
return payload["username"]
except:
return None
def 需要认证(f):
"""装饰器:检查请求头里有没有有效的 Token"""
def wrapper(*args, **kwargs):
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return jsonify({"error": "缺少Token"}), 401
token = auth_header.split(" ")[1]
username = 验证Token(token)
if not username:
return jsonify({"error": "Token无效或已过期"}), 401
return f(username, *args, **kwargs)
wrapper.__name__ = f.__name__
return wrapper
@app.route("/login", methods=["POST"])
def login():
data = request.get_json()
username = data.get("username")
password = data.get("password")
if username in 用户库 and 用户库[username] == password:
token = 生成Token(username)
return jsonify({"token": token})
return jsonify({"error": "账号或密码错误"}), 401
@app.route("/tasks", methods=["GET"])
@需要认证
def 获取任务(username):
tasks = 任务库.get(username, [])
return jsonify({"username": username, "tasks": tasks})
@app.route("/tasks", methods=["POST"])
@需要认证
def 添加任务(username):
data = request.get_json()
new_task = {
"id": len(任务库.get(username, [])) + 1,
"内容": data.get("内容"),
"完成": False
}
if username not in 任务库:
任务库[username] = []
任务库[username].append(new_task)
return jsonify({"message": "任务添加成功", "task": new_task})
@app.route("/tasks/<int:task_id>", methods=["PUT"])
@需要认证
def 更新任务(username, task_id):
data = request.get_json()
tasks = 任务库.get(username, [])
for task in tasks:
if task["id"] == task_id:
task["完成"] = data.get("完成", task["完成"])
return jsonify({"message": "任务更新成功", "task": task})
return jsonify({"error": "任务不存在"}), 404
if __name__ == "__main__":
print("登录: POST /login (username, password)")
print("获取任务: GET /tasks (Header: Authorization: Bearer <token>)")
print("添加任务: POST /tasks (内容)")
print("更新任务: PUT /tasks/<id> (完成: true/false)")
app.run(port=5001, debug=True)
测试:
# 1. 登录获取 Token
TOKEN=$(curl -s -X POST http://localhost:5001/login \
-H "Content-Type: application/json" \
-d '{"username":"小明","password":"123"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
echo "获取到的Token: $TOKEN"
# 2. 用 Token 获取任务
curl -H "Authorization: Bearer $TOKEN" http://localhost:5001/tasks
# 3. 添加新任务
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"内容":"学习JWT"}' \
http://localhost:5001/tasks
# 4. 用过期或假的 Token 试试
curl -H "Authorization: Bearer fake_token" http://localhost:5001/tasks
预期输出:
# 登录
{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
# 获取任务
{"username": "小明", "tasks": [{"id": 1, "内容": "写周报", "完成": false}]}
# 添加任务
{"message": "任务添加成功", "task": {"id": 2, "内容": "学习JWT", "完成": false}}
# 用假 Token
{"error": "Token无效或已过期"}
一句话解释: Token 过期了就得重新登录,这就避免了「永久有效」的 token 被盗用后别人能一直冒用的问题。
项目 3(15 分钟):组合做个「轻量级用户系统」
这回综合一下,做一个能注册、能登录、能查看个人信息的完整小工具。
# 文件名: mini_user_system.py
# 运行: python mini_user_system.py
import json
import jwt
import datetime
import hashlib
import os
# JWT 配置
秘钥 = "mini_system_secret_key_789"
Token过期小时 = 24
# 用户数据文件
用户文件 = "users.json"
def 加载用户():
"""从文件加载用户数据"""
if os.path.exists(用户文件):
with open(用户文件, "r", encoding="utf-8") as f:
return json.load(f)
return {}
def 保存用户(users):
"""保存用户数据到文件"""
with open(用户文件, "w", encoding="utf-8") as f:
json.dump(users, f, ensure_ascii=False, indent=2)
def 加密密码(password):
"""简单密码加密(生产环境用 bcrypt)"""
return hashlib.sha256(password.encode()).hexdigest()
def 生成Token(username):
payload = {
"username": username,
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=Token过期小时)
}
return jwt.encode(payload, 秘钥, algorithm="HS256")
def 验证Token(token):
try:
payload = jwt.decode(token, 秘钥, algorithms=["HS256"])
return payload["username"]
except:
return None
class 用户系统:
def __init__(self):
self.users = 加载用户()
def 注册(self, username, password):
if username in self.users:
return False, "用户名已存在"
self.users[username] = {
"password": 加密密码(password),
"created": datetime.datetime.now().isoformat()
}
保存用户(self.users)
return True, "注册成功"
def 登录(self, username, password):
if username not in self.users:
return None, "用户不存在"
if self.users[username]["password"] != 加密密码(password):
return None, "密码错误"
token = 生成Token(username)
return token, "登录成功"
def 验证请求(self, token):
username = 验证Token(token)
if username and username in self.users:
return username
return None
def 获取信息(self, username):
if username in self.users:
return {
"username": username,
"created": self.users[username]["created"]
}
return None
def main():
系统 = 用户系统()
print("=" * 40)
print(" 轻量级用户系统 v1.0")
print("=" * 40)
print()
while True:
print("1. 注册 2. 登录 3. 查看个人信息 4. 退出")
choice = input("请选择操作: ").strip()
if choice == "1":
username = input("用户名: ").strip()
password = input("密码: ").strip()
success, msg = 系统.注册(username, password)
print(f"结果: {msg}")
elif choice == "2":
username = input("用户名: ").strip()
password = input("密码: ").strip()
token, msg = 系统.登录(username, password)
if token:
print(f"结果: {msg}")
print(f"你的Token: {token[:50]}...")
else:
print(f"结果: {msg}")
elif choice == "3":
token = input("请粘贴Token: ").strip()
username = 系统.验证请求(token)
if username:
info = 系统.获取信息(username)
print(f"结果: 欢迎 {info['username']},注册于 {info['created']}")
else:
print("结果: Token无效或已过期")
elif choice == "4":
print("再见!")
break
print()
if __name__ == "__main__":
main()
运行效果:
========================================
轻量级用户系统 v1.0
========================================
1. 注册 2. 登录 3. 查看个人信息 4. 退出
请选择操作: 1
用户名: 小明
密码: abc123
结果: 注册成功
请选择操作: 2
用户名: 小明
密码: abc123
结果: 登录成功
你的Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IuW8o...
请选择操作: 3
请粘贴Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
结果: 欢迎 小明,注册于 2024-01-15T10:30:00
请选择操作: 4
再见!
一句话解释: 把用户数据存到 JSON 文件里,这样程序重启后用户数据还在,实现了「持久化」。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:Token 存在 localStorage 里会被 XSS 攻击偷走
❌ 错误做法:
// 前端代码(不要这样写!)
localStorage.setItem("token", response.data.token);
✅ 正确做法:
// 把 Token 存在 HttpOnly Cookie 里,只有同源的请求才会自动带上
// 后端设置:res.cookie("token", token, { httpOnly: true });
解释: HttpOnly 的 Cookie JavaScript 读不到,XSS 攻击也就偷不走。
坑 2:Session 的 secret_key 太简单
❌ 错误做法:
app.secret_key = "123" # 太简单,容易被猜到
✅ 正确做法:
import secrets
app.secret_key = secrets.token_hex(32) # 生成32字节的随机密钥
坑 3:JWT 过期时间设太长
❌ 错误做法:
payload = {
"username": username,
"exp": datetime.datetime.utcnow() + datetime.timedelta(days=365) # 一年?太久了!
}
✅ 正确做法:
# _ACCESS_TOKEN 到期时间设短一点(比如15分钟)
# _REFRESH_TOKEN 到期时间长一点(比如7天)
# access token 过期了,用 refresh token 换一个新的
坑 4:密码用明文存储
❌ 错误做法:
if user.password == password: # 明文密码比对
return "登录成功"
✅ 正确做法:
# 用 bcrypt 或 argon2 加密
import bcrypt
if bcrypt.checkpw(password.encode(), user.password_hash.encode()):
return "登录成功"
坑 5:Session 没有设置过期
❌ 错误做法:
session["user"] = username # 没有设置过期时间
✅ 正确做法:
from datetime import timedelta
session.permanent = True
app.permanent_session_lifetime = timedelta(hours=2)
性能小贴士:Session 存储用 Redis
如果用户量很大,文件存储的 Session 会变慢。把Session 存到 Redis 里:
# pip install redis flask-session
app.config["SESSION_TYPE"] = "redis"
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_USE_SIGNER"] = True
app.config["SESSION_KEY_PREFIX"] = "session:"
app.config["SESSION_REDIS"] = redis.Redis(host="localhost", port=6379)
调试技巧:用 print 大法
def 验证Token(token):
print(f"[DEBUG] 收到的Token: {token[:20]}...") # 打印前20个字符
try:
payload = jwt.decode(token, 秘钥, algorithms=["HS256"])
print(f"[DEBUG] 解码成功: {payload}")
return payload["username"]
except jwt.ExpiredSignatureError:
print("[DEBUG] Token已过期")
return None
except jwt.InvalidTokenError as e:
print(f"[DEBUG] Token无效: {e}")
return None
✏️ 练习题
练习 1(2 分钟):改 Token 过期时间
- 输入:把项目 2 的 Token 过期时间从 2 小时改成 30 分钟
- 预期输出:代码能正常运行,Token 30 分钟后过期
- 提示:找 timedelta(hours=2) 改成 minutes=30
练习 2(2 分钟):加一个「修改密码」功能
- 输入:在项目 1 基础上加一个新路由 /change_password,需要先验证旧密码
- 预期输出:POST 请求能修改密码
- 提示:新建一个路由,用 session.pop 清掉旧 session 后要求重新登录
练习 3(3 分钟):给任务 API 加个「统计」功能
- 输入:用项目 2 的数据,统计每个人的任务总数和已完成数
- 预期输出:{"username": "小明", "total": 5, "done": 2}
- 提示:用列表推导式或循环统计 完成==True 的任务数
练习 4(3 分钟):把项目 2 的 JWT 改成项目 1 的 Session 实现
- 输入:把项目 2 的 Token 验证改成 Session 验证
- 预期输出:登录后访问 /tasks 能正常返回数据
- 提示:把 Authorization: Bearer <token> 改成 Cookie 里的 session
练习 5(5 分钟):Token 被篡改后会怎样?
- 输入:手动改一下 JWT 的 payload 部分(比如把 "小明" 改成 "管理员"),然后验证
- 预期输出:验证失败,报 InvalidSignatureError
- 提示:JWT 的第三段是签名,改了 payload 但没改签名,验证会失败
作业:做一个「带 Token 刷新功能的记事本」
需求描述:
用 Flask 实现一个命令行记事本,用户的记事内容存在 JSON 文件里,每个用户只能看到自己的内容。
功能点:
1. 用户注册/登录:注册时密码用 bcrypt 加密,登录后返回 JWT
2. 记事本 CRUD:登录后可以查看、添加、删除、标记完成记事
3. Token 刷新:access token 5 分钟过期,但可以用 refresh token 换新的
加分项:
1. 用 Redis 存 refresh token(而不是放内存里)
2. 支持多设备登录,但可以强制某设备下线
验收标准:
- 注册登录流程能跑通
- 记事本能正常增删改查
- Token 过期后能用 refresh token 获取新 token
- 代码有适当注释
📚 总结 + 资源
本文学了 3 件事:
1. Session 像房卡,服务器帮你记着谁登录了,你带着 Session ID 就行
2. JWT 像通票,所有信息都在票上,服务器只验证签名真假
3. 密码要加密存储,Token 要设过期时间,这是基本的安全规范
延伸学习资源:
- PyJWT 官方文档 - 权威参考
- 《Web 安全设计之道》第三章 - 讲了很多实际的安全漏洞案例
- Flask 官方文档 - Session - 看看 Flask 原生 Session 怎么用
互动钩子:
你在公司用过 Session 还是 JWT?遇到过什么坑?评论区聊聊,老粉优先回复!
下一章我们要聊的话题更刺激:XSS、CSRF、SQL 注入——这些名字听起来吓人,但理解了原理之后,你会发现它们都是「纸老虎」。下一章见!

评论(0)