码上问学问代码之理,学编程之道

Flask 基础概念图解

写接口之前,有四个概念你绕不开:CORS(跨域访问的边界)、Cookie(浏览器里的「会员卡」)、Session(服务器端的「存包柜」)、JWT(带签名的「通行证」)。它们管的都是同一件事——「你是谁、你能访问什么、我凭什么信你」。本章用费曼方式(先讲生活类比,再讲代码)把它们一次讲透。

先读这一页,再进第 7、8 章。第 7 章讲 CORS 与 RESTful,第 8 章讲 JWT 鉴权,它们都需要这一页的类比打底。你可以把这一页当成「概念地图」,回头忘了随时翻。

CORS:小区门禁的「放行条」

Cross-Origin Resource Sharing,跨域资源共享
先讲为什么浏览器里有一条「同源策略」——就像小区保安默认只让本小区业主进出。同源指的是「协议 + 域名 + 端口」三者都一样;只要有一个不同,就叫不同源。开发时前端跑在 http://localhost:5173,后端跑在 http://localhost:5000,端口不同,所以是不同源。前端想从后端拿数据,保安(浏览器)会把响应拦下来:「你不是我们小区的。」
CORS 就是后端给保安写的一张放行条:「这个人是我请来的,放他进去。」保安看条子放行。
动手做在 Flask 里安装 Flask-Cors,然后在创建 app 时注册:
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": ["http://localhost:5173", "http://localhost:5000"]}})
这样,前端只要从 5173 或 5000 发 /api/* 请求,响应头里就会带上 Access-Control-Allow-Origin: http://localhost:5173,浏览器就放行。
讲透
  • 被拦的是「响应」,不是「请求」。前端其实已经把请求发到了后端,后端也执行了;只是浏览器在拿到响应后,发现响应头里没有 CORS 许可,才把结果藏起来不给 JS。这就是为什么你在 Network 面板能看到 200,但代码里却报错。
  • CORS 不是安全机制,是访问控制声明。它防的是「恶意网站用你的登录态去调银行接口」这类场景。如果你把 origins="*" 设成允许所有人,就等于把放行条写成「谁都能进」,生产环境不要这样做。
  • 开发期为什么常常感觉不到 CORS?因为 Vite 的 dev server 自带代理——它把你的 /api/xxx 请求先收到自己这儿,再帮你转发给 Flask。浏览器眼中你是在跟 5173 说话,是同源,所以根本没触发跨域。
  • 上线后前端和后端通常共用同一个域名(如 https://wenxue.example.com),Nginx 把 /api/* 反向代理到 Flask,也不需要 CORS。只有在「前端一个域名、后端另一个域名」时才必须配置。
避坑看到 No 'Access-Control-Allow-Origin' header 报错,不要慌:① 确认后端真加了 CORS;② 确认请求的是 /api/* 路径;③ 确认开发时没有绕过 Vite 代理直接访问 5000;④ 生产环境千万不要用 supports_credentials=True 配 origins="*",这是重大安全隐患。

Session:服务器端的「存包柜」

数据存在服务器,浏览器只拿柜号
先讲为什么Cookie 只能放小纸条,不能真的存购物车、登录状态这些复杂信息。就像你去商场,不能背着所有行李逛,于是商场给你一个存包柜,柜门贴上号码 A-12。你手里拿着的是号码,真正的包(数据)存在商场后场的柜子里。
Session 就是这个柜子:服务器在内存或数据库里给每个用户开辟一块空间,然后把「柜号」session_id 通过 Cookie 交给浏览器。下次请求,浏览器把柜号带回来,服务器打开对应柜子,取出你的数据。
动手做Flask 自带 Session(基于 Cookie + 服务端签名):
from flask import Flask, session

app = Flask(__name__)
app.secret_key = "你的随机密钥"   # 用于给 session cookie 签名

@app.route("/login")
def login():
    session["user_id"] = 9527
    return {"msg": "已存柜"}

@app.route("/me")
def me():
    return {"user_id": session.get("user_id")}
这里 Flask 实际做了两件事:① 在浏览器 Cookie 里放一个加密的 session_id;② 默认把 Session 数据也编码进 Cookie(Flask 客户端 session),服务端不额外存。如果换成 RedisSession 插件,数据才真正存在服务器端的 Redis 里。
讲透
  • Session 解决的是「HTTP 无状态」。它让服务器能在一段时间内记住你,哪怕你刷新了页面、点了十几次按钮。
  • Session 依赖 Cookie。浏览器如果不支持 Cookie,或者用户清除了 Cookie,柜号就丢了,服务器找不到柜子。
  • 数据存在服务器更安全。用户拿不到柜子里面的东西,只能拿到柜号。柜号本身也是随机、加密的,没法伪造。
  • Session 的缺点是有状态。如果有 3 台 Flask 服务器,用户这次请求到 A,下次到 B,B 没有 A 的柜子,用户就被踢下线了。所以集群部署时通常把 Session 存在 Redis 等共享存储里。
避坑不要把 app.secret_key 设成 "123456" 并提交到 Git。Secret Key 是用来签名 Session Cookie 的,被别人拿到就能伪造柜号、登录你的账号。生产环境从环境变量读取,例如 app.secret_key = os.environ["FLASK_SECRET_KEY"]。

JWT:带防伪印章的「通行证」

JSON Web Token,服务端不存、客户端带着走
先讲为什么Session 好用,但它要求服务器记住每一个人。人多了柜子就多,多台服务器还得共用一个仓库(Redis),压力大。JWT 的思路是:别让我记你了,你自己带一张「通行证」。就像你去演唱会,检票口不发储物柜,而是给你一张印有座位号、时间和防伪二维码的门票。你进场时自己掏出门票,检票口一扫就知道你是谁、票是不是假的,不用再去后台查名单。
动手做用 PyJWT 签发与校验:
import jwt, datetime
from flask import request

SECRET = "你的长随机密钥"

# 登录成功后签发 token
def make_token(user_id):
    payload = {
        "user_id": user_id,
        "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=24)
    }
    return jwt.encode(payload, SECRET, algorithm="HS256")

# 每次请求时校验
def verify_token():
    token = request.headers.get("Authorization", "").replace("Bearer ", "")
    try:
        return jwt.decode(token, SECRET, algorithms=["HS256"])
    except jwt.ExpiredSignatureError:
        return None   # 票过期了
    except jwt.InvalidTokenError:
        return None   # 假票
前端拿到 token 后,每次发请求都在请求头里带上 Authorization: Bearer <token>。Flask 验签即可,不用查数据库。
讲透
  • JWT 的结构是三段 Base64:header.payload.signature。Header 写算法,Payload 写用户信息和过期时间,Signature 是用密钥对前两段做的签名。
  • Payload 是可读的,不要放密码。任何人都能把中间那段 Base64 解码,看到里面写了什么。它只适合放用户 ID、角色、过期时间这类不敏感但用户有权看到的信息。
  • JWT 的安全性来自 Signature。攻击者可以伪造 payload,但没法伪造 signature,因为他没有服务器的密钥。只要签名对不上,Flask 直接拒绝。
  • JWT 是无状态的。服务器不需要存任何会话信息,随便哪台 Flask 机器都能独立验签。这正是它适合前后端分离、微服务和移动端的原因。
  • JWT 一旦签发,就无法提前作废。如果用户 token 泄露,只能等它自然过期。所以 token 有效期要设短(比如 24 小时),并配合「刷新 token」机制。
避坑① 密钥不要硬编码,放环境变量或 .env;② 不要放敏感信息到 payload;③ 不要把 token 存到 localStorage 就万事大吉——XSS 脚本能读到 localStorage,必要时用内存 + HttpOnly Cookie 双 token;④ 接口验证失败要返回 401 Unauthorized,不要返回 200 再让前端猜。

一张表看懂它们

CORS 是边界问题,后三个是身份问题
概念一句话数据存在哪适合场景
CORS后端告诉浏览器:「这个前端域名可以读我」响应头里前后端分离、不同端口/域名
Cookie服务器发的小纸条,浏览器以后每次请求自动带上浏览器保存无意义标识、记住登录偏好
Session服务器用 Cookie 里的柜号,打开属于你的「存包柜」服务器(内存/Redis/DB)需要服务器记住状态、可即时失效
JWT服务器签发带签名的通行证,你自己拿着走客户端前后端分离、微服务、移动端、无状态集群
记忆口诀:CORS 管「能不能来」,Cookie 是「带的卡」,Session 是「存的柜」,JWT 是「拿的票」。「古典文学网」用的组合是:上线期 Nginx 反代解决同源、JWT 解决登录态——无状态、好扩展。

本章小结

动手练习
1用「小区门禁/外卖条」这个类比,向一个没学过 Web 开发的人解释 CORS。要求不能出现「同源策略」四个字。
2为什么 Session 能解决 HTTP 无状态问题?浏览器如果禁用 Cookie,Session 还能工作吗?为什么?
3登录后,服务器返回的 JWT 被攻击者截获了。相比 Session,JWT 有什么优势和劣势?你会怎么降低损失?
参考答案

第 1 题:两个小区隔着一条马路。A 小区的访客想进 B 小区拿快递,B 小区的保安说:「我只让名单上的人进。」于是 B 小区物业把访客的车牌号登记到放行名单里,保安看到车牌就让进。CORS 就是这个放行名单。

第 2 题:Session 给每个用户一个「柜号」,服务器用柜号找到属于他的数据,所以在多次请求之间能记住用户。浏览器禁用 Cookie 后,柜号无法带回,服务器找不到柜子,Session 就失效了。

第 3 题:JWT 的优势是服务器不存会话,任何一台机器都能验签;劣势是 token 无法中途作废,只能等过期。降低损失的做法:① token 有效期设短(如 2 小时);② 用 Refresh Token + Access Token 双 token;③ 敏感接口(改密码、转账)必须要求重新输入密码或短信验证。