做后端接口开发,绕不开认证授权这件事。早期做 Flask 项目,大家习惯用 session 加 cookie,后来前端分离、移动端兴起,token 变成了主流。而 Flask-JWT-Extended 这个库,几乎是我见过在 Flask 生态里把 JWT 做得最省心的方案。它不光能生成 token、校验 token,还把刷新、黑名单、可选认证这些常见需求都封装成了几行代码的事。这篇文章我会从选型思路、核心概念、实操落地到问题排查,完整过一遍,希望你看完能直接上手,也能避开我当初踩过的那些坑。
不管你是刚接触 Flask 的新手,还是已经在用别的认证方案想换过来的老手,这篇都适用。我会尽量把每个“为什么”讲清楚,比如为什么要用 refresh token、为什么 @jwt_required 有时候会失效、为什么黑名单不是默认开启的。这些东西文档里写得比较简单,但实际用起来全是细节。
1. 为什么要选 Flask-JWT-Extended:认证方案选型的心路历程
1.1 从 Session 到 Token:认证方式的演进逻辑
早些年的 Flask 项目,最常见的做法是服务端 session。用户登录成功后,服务端把用户信息存在 session 里,同时下发一个 session id 写到 cookie,浏览器每次请求自动带上 cookie,服务端根据 session id 找到对应的用户数据。这个过程很顺滑,特别是纯后端渲染页面的时代,几乎不用前端操心。
但到了前后端分离、移动端 App 盛行的时候,session 方案就有点别扭了。App 里没有 cookie 的概念,你得手动把 session id 存起来再塞到请求头里,这本身就失去了 session 方案“浏览器自动处理”的优势。更麻烦的是,session 数据默认存在服务端内存里,一旦服务重启,所有用户都要重新登录;多台服务器部署时,还得引入 redis 之类的共享存储,否则用户在 A 机器登录,请求打到 B 机器就识别不了。
Token 方案把用户信息直接加密放进 token 里,服务端不存状态,只要验签通过就认账。这样天然适合分布式部署,也适合 App 和第三方开发者接入。JWT(JSON Web Token)是其中最通用的一种格式,它由 header、payload、signature 三段组成,每一段都是 base64 编码的 JSON,最后一段是用密钥对前面内容做的签名。token 一旦签发,只要密钥不泄露,没人能伪造。
Flask-JWT-Extended 就是基于 PyJWT 封装的一套 Flask 扩展,它把 token 的生成、校验、刷新、黑名单这些逻辑都统一到了一个装饰器体系里。你不需要关心 JWT 的编码细节,也不需要自己写一堆校验函数,只需要在视图函数上加上 @jwt_required(),就能拦截未登录请求。这一点对于团队协作特别重要,因为你不需要每个人都理解 JWT 内部机制,只需要约定好“受保护的接口加装饰器”就行。
1.2 和 PyJWT、Flask-JWT 对比:Flask-JWT-Extended 强在哪
有些同学会自己去用 PyJWT 写封装,感觉也没多复杂。确实,如果你只是需要“生成一个 token、验签解析一下”,PyJWT 几十行代码就能搞定。但真实项目里往往还有这些需求:token 过期时间怎么管理?哪些接口允许过期 token 访问?刷新 token 的接口怎么设计?用户被禁言了怎么立即失效 token?这些如果全部自己写,很容易出现逻辑不一致的 bug。
还有一个老库叫 Flask-JWT,它比 Flask-JWT-Extended 出现得早,但功能上弱很多。Flask-JWT 只提供了最基本的 authenticate 和 identity 回调, token 过期策略、刷新机制、可选保护都没有原生支持。而且这个库维护得不是很积极,Python 3 和较新 Flask 版本下兼容性一般。Flask-JWT-Extended 则是由维护者持续更新的,支持 Flask 2.x 和 3.x,文档也更完善。
我自己的体会是,选 Flask-JWT-Extended 最大的理由并不是“它功能多”,而是它的设计思路恰好贴合现实业务。比如它把“必需认证”和“可选认证”分成了 @jwt_required() 和 @jwt_optional(),这解决了一个很典型的问题:一个资源接口,未登录用户也能看,登录用户能看到专属内容。如果用 PyJWT 自己写,你可能要写两套逻辑或者手动判断 token 是否存在,而在 Flask-JWT-Extended 里就是换一个装饰器的事情。
另一个亮点是 token 刷新机制做得非常自然。它允许你签发一组 token:一个短寿命的 access token 用于实际请求,一个长寿命的 refresh token 用于换取新的 access token。这样即便 access token 泄露,攻击者也只能在短时间内滥用,而 refresh token 我们可以额外做轮换和黑名单控制。PyJWT 本身可不会替你考虑这些。
2. 核心概念与关键配置:不只是“生成 token 那么简单”
2.1 token 从哪来:access token 与 refresh token 的分工
先明确一个概念:JWT 一旦签发,在过期之前都是有效的。服务端无法主动让一个已签发的 token 失效,除非引入黑名单机制。这意味着 token 的过期时间设置必须短,否则一个 token 泄露了,你只能眼睁睁看着它在几小时内被人滥用。
所以成熟的方案是把 token 分成两种。access token 寿命短,一般设置 15 分钟到 1 小时,用于调用业务接口。refresh token 寿命长,通常几天甚至几周,它只有一个用途:去换取新的 access token。这样一来,access token 泄露的风险窗口很小,refresh token 泄露则可以通过黑名单机制封禁。
Flask-JWT-Extended 里,签发 access token 用create_access_token(identity=user_id),签发 refresh token 用create_refresh_token(identity=user_id)。identity 不一定是用户 ID,可以是任何能代表用户身份的数据,比如一个包含用户 ID 和角色的字典。我习惯把 identity 设为字符串形式的用户 ID,因为 JWT 的 payload 里 identity 字段会被直接 JSON 序列化,如果是自定义对象,容易出现序列化问题。
from flask_jwt_extended import create_access_token, create_refresh_token def login(): user = User.query.filter_by(username=request.json["username"]).first() if user and user.check_password(request.json["password"]): access_token = create_access_token(identity=str(user.id)) refresh_token = create_refresh_token(identity=str(user.id)) return {"access_token": access_token, "refresh_token": refresh_token} return {"msg": "用户名或密码错误"}, 401注意identity参数不只是存一个 ID,后续每次请求带着 access token 进来时,你可以通过get_jwt_identity()拿到这个值,然后在视图里查数据库、做权限判断。所以 identity 最好是稳定且唯一的用户标识。
2.2 保护接口的三种姿势:@jwt_required、@jwt_optional 与 fresh token
Flask-JWT-Extended 提供了几种装饰器,它们解决的是不同场景的问题。
@jwt_required()是最常用的,任何加了它的视图,请求必须在 Authorization 头里带上合法的 access token,否则直接返回 401。这里有个容易混淆的点:refresh token 是不能通过@jwt_required()校验的。如果你拿着 refresh token 去请求业务接口,会被当成无效 token。这是设计上刻意为之,避免两种 token 混用。
@jwt_optional()解决的是“可登录可不登录”的场景。比如一个文章详情接口,未登录用户可以看,登录用户可以看专属内容。用 @jwt_optional() 后,请求即使不带 token 也能进入视图,只是get_jwt_identity()会返回 None。你需要自己在代码里判断:
from flask_jwt_extended import jwt_optional, get_jwt_identity @app.route("/article/<int:article_id>") @jwt_optional() def article_detail(article_id): user_id = get_jwt_identity() if user_id: article = Article.query.filter_by(id=article_id, user_id=user_id).first() else: article = Article.query.get(article_id) return ...还有一种是 fresh token。有些安全敏感操作,比如修改密码、绑定手机号,你希望用户必须刚登录过,而不是用一个刷新了好几次的长寿 token。这时你可以给 access token 加一个fresh=True参数,然后视图用@jwt_required(fresh=True)装饰。假如用户 token 不新鲜,就得重新登录再操作。这个机制很实用,能有效防止 token 在长时间有效期内被滥用。
access_token = create_access_token(identity=str(user.id), fresh=True) refresh_token = create_refresh_token(identity=str(user.id))2.3 配置项里最容易踩坑的几个参数
Flask-JWT-Extended 的配置项都是 Flask 的 config 风格,在app.config里设置。有几个参数我反复调试过,现在列出来给你作为参考。
JWT_SECRET_KEY是最关键的。它是签名的密钥,生产环境必须设置成足够长的随机字符串,而且不能泄露。如果没设置,扩展会使用 Flask app 的SECRET_KEY;如果两个都没设,扩展直接报错。我见过有人把密钥写死在代码里,这非常危险,至少也应该放到环境变量里。
JWT_TOKEN_LOCATION默认是["headers"],也就是只从请求头的 Authorization 里读取 token。但你也可以配置成["headers", "cookies"]或者["headers", "query_string"],也就是说支持从 cookie 或 URL 查询参数里取 token。如果你做的是纯前端分离项目,保持 headers 就好;如果你的某些页面是服务端渲染,走 cookie 会更方便。不过从查询参数传 token 是不推荐的,因为 token 会出现在日志里。
JWT_ACCESS_TOKEN_EXPIRES和JWT_REFRESH_TOKEN_EXPIRES控制两种 token 的过期时间。默认分别是 15 分钟和 30 天。如果我们希望 access token 短一点,可以设置成datetime.timedelta(minutes=30);refresh token 可以设置成datetime.timedelta(days=7)。这里要特别注意,如果你使用expires=timedelta(...)来单独指定某个 token 的过期时间,那么它会覆盖全局配置。
from datetime import timedelta app.config["JWT_SECRET_KEY"] = "super-secret-key-change-me" app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(minutes=30) app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=7)JWT_BLACKLIST_ENABLED默认是 False。如果你要使用黑名单功能,必须显式开启,并且实现两个回调函数:一个是@jwt.token_in_blocklist_loader,另一个通常是@jwt.revoked_token_loader。很多人一开始忘了开启这个开关,代码写得没问题,但黑名单就是不生效,其实就是这个配置漏了。
3. 实操:从零搭建一个带刷新机制的认证接口
3.1 初始化扩展与用户模型设计
我直接用一个简化的用户模型来演示。假设我们用的是 Flask-SQLAlchemy,用户表包含 id、username、password_hash 和 is_active。实际项目里还会有邮箱、头像、角色等字段,这里从简。
先初始化扩展:
from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy from flask_jwt_extended import JWTManager, create_access_token, create_refresh_token, get_jwt_identity, jwt_required app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db" app.config["SECRET_KEY"] = "please-change-me" app.config["JWT_SECRET_KEY"] = "please-change-me-too" app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(minutes=30) app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=7) app.config["JWT_BLACKLIST_ENABLED"] = True db = SQLAlchemy(app) jwt = JWTManager(app)用户模型加上密码哈希校验。这里我直接用 werkzeug 的generate_password_hash和check_password_hash,简单可靠。
from werkzeug.security import generate_password_hash, check_password_hash class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) is_active = db.Column(db.Boolean, default=True) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password)注意一点,JWT 的 identity 我建议使用字符串形式的用户 ID,而不是直接传整数。因为 JWT 的 payload 通过 base64 编码,最终解析出来的是 JSON 类型,整数其实也能存,但某些客户端语言(比如 JavaScript)处理大整数时会有精度问题。用户 ID 一般不会超过 JS 的安全整数范围,但统一转成字符串能避免踩坑,而且get_jwt_identity()拿到的值就可以直接用于查询或作为字典键。
3.2 注册登录:access token + refresh token 的完整发放流程
注册接口不需要加认证装饰器,逻辑很简单:检查用户名是否存在,不存在就创建用户并保存。
@app.post("/register") def register(): username = request.json.get("username") password = request.json.get("password") if not username or not password: return {"msg": "用户名和密码不能为空"}, 400 if User.query.filter_by(username=username).first(): return {"msg": "用户名已存在"}, 409 user = User(username=username) user.set_password(password) db.session.add(user) db.session.commit() return {"msg": "注册成功"}, 201登录接口是重点。校验用户名密码成功后,用户签发一组 token。这里我还把用户状态检查了一下,如果 is_active 为 False,说明用户被禁用了,就不给签发 token。
@app.post("/login") def login(): username = request.json.get("username") password = request.json.get("password") user = User.query.filter_by(username=username).first() if not user or not user.check_password(password): return {"msg": "用户名或密码错误"}, 401 if not user.is_active: return {"msg": "用户已被禁用"}, 403 access_token = create_access_token(identity=str(user.id), fresh=True) refresh_token = create_refresh_token(identity=str(user.id)) return { "access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer", "expires_in": 1800 }登录成功后,前端通常会把 access_token 保存在内存里(比如 Vuex、Redux),把 refresh_token 保存在更持久的地方(比如 localStorage)。每次请求在 axios 拦截器里把 access_token 放到 Authorization 头。当 access_token 过期后,前端用 refresh_token 去调刷新接口,拿新的 access_token,然后重放原来的请求。这套流程在 Web 端很常见。
3.3 刷新与注销:refresh token 的轮换和黑名单机制
刷新接口有点特殊:它不能用@jwt_required(),因为 refresh token 不是 access token。Flask-JWT-Extended 提供了@jwt_required(refresh=True)这个变体,专门用来校验 refresh token。在刷新逻辑里,我们拿到当前 refresh token 的 jti(token 的唯一 ID),先把它加入黑名单,再签发一组全新的 token。这就是“refresh token 轮换”:每次刷新,旧 refresh token 立即失效,即使泄露了也无法再次使用。
要为黑名单功能写回调。我们需要一个 TokenBlocklist 模型来存储被拉黑的 jti 和过期时间。其实也可以用 Redis,但为了演示简单,我用数据库表。
class TokenBlocklist(db.Model): id = db.Column(db.Integer, primary_key=True) jti = db.Column(db.String(36), nullable=False, index=True) token_type = db.Column(db.String(10), nullable=False) user_id = db.Column(db.String(36), nullable=False) created_at = db.Column(db.DateTime, nullable=False) expires_at = db.Column(db.DateTime, nullable=False)然后实现黑名单回调:
@jwt.token_in_blocklist_loader def check_if_token_revoked(jwt_header, jwt_payload): jti = jwt_payload["jti"] token = TokenBlocklist.query.filter_by(jti=jti).first() return token is not None这个回调会在每次校验 token 时被调用。如果返回 True,说明 token 在黑名单里,请求会被拒绝。
刷新接口和注销接口实现如下:
@app.post("/refresh") @jwt_required(refresh=True) def refresh(): identity = get_jwt_identity() access_token = create_access_token(identity=identity, fresh=False) return {"access_token": access_token, "expires_in": 1800}看到没有?这里我签发的新 access token 的fresh=False,它表示这个 token 不是最近登录获得的,而是通过刷新得到的。如果某个高敏感操作要求 fresh=True,用户就需要重新登录。
那么“注销登录”该怎么实现呢?核心思路是把当前使用的 token 加入黑名单。如果是注销 access token,那么只需要把当前 access token 的 jti 记录下来;如果是注销 refresh token,也一样。为了简单,我们提供一个注销接口,要求携带 access token,并且把它的 jti 存进黑名单:
from flask_jwt_extended import get_jwt @app.delete("/logout") @jwt_required() def logout(): jti = get_jwt()["jti"] now = datetime.now(timezone.utc) token_block = TokenBlocklist( jti=jti, token_type="access", user_id=get_jwt_identity(), created_at=now, expires_at=now + timedelta(minutes=30) ) db.session.add(token_block) db.session.commit() return {"msg": "已注销"}, 200如果你要在刷新时同时注销旧 refresh token,可以再写一个@jwt_required(refresh=True)的接口,在签发新 token 前把当前 refresh token 的 jti 加入黑名单。现实中建议客户端在收到新的 refresh token 后,把旧的从本地删掉,避免出现“新老 token 同时有效”的情况。
3.3.1 黑名单清理策略:别让数据表无限膨胀
黑名单表如果不清理,时间久了会非常大,因为每次注销和刷新都会插入一条记录。实际项目中最好定期清理过期记录。比如用定时任务:
def clear_expired_tokens(): now = datetime.now(timezone.utc) TokenBlocklist.query.filter(TokenBlocklist.expires_at < now).delete() db.session.commit()这个函数可以挂到 cron 或者 Celery 定时任务里,每天运行一次就够。
4. 常见问题与排查技巧实录
4.1 为什么我的 @jwt_required 不生效?
遇到最多的情况是:接口加了 @jwt_required(),但没带 token 也能访问。先说原因,99% 是装饰器顺序写错了。Flask 的装饰器是由内往外执行的,如果视图函数上方有多个装饰器,@jwt_required() 必须放在最靠近函数的位置,否则它会被外层的装饰器“跳过”。
下面这段错误示例:
@app.route("/protected") @jwt_required() def protected(): return {"msg": "ok"}这段其实是正确的。错误的是这种:
@jwt_required() @app.route("/protected") def protected(): return {"msg": "ok"}这种情况下,Flask 内部先注册路由,再套 JWT 装饰器,也可能导致行为异常。另外,如果用了蓝图和自定义装饰器,也要注意顺序。JWT 装饰器必须在 Flask 的 route 装饰器之后,同时尽量放在最内层。
还有一种情况是扩展还没初始化。你在 create_app 工厂函数里忘了调用JWTManager(app),导致 @jwt_required() 使用了默认的空配置,可能不会触发校验。解决办法是在 app 创建后立刻初始化 jwt。
4.2 头部传了 token 还是 401?揭开 Authorization 解析规则
Flask-JWT-Extended 默认从 Authorization 头读取 token,但它期望的格式是Authorization: Bearer <token>。如果你直接传Authorization: <token>,也就是没有 Bearer 前缀,扩展会报错或者返回 401。这是一个非常普遍的坑。
还有一个细节:大小写。Bearer 首字母大写,但扩展内部做了解析,其实大小写不敏感,关键是有空格分隔。如果你用 Postman 测试,记得在 Authorization 类型里选择 Bearer Token,它会自动帮你加前缀。如果是通过 axios 拦截器手动设置,务必写成:
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;如果你需要自定义 token 读取方式,可以设置JWT_AUTH_HEADER_NAME和JWT_AUTH_HEADER_PREFIX,但我建议保持默认,因为这是行业惯例。
4.3 token 过期了但前端还在用?关于 expires 和 leeway 的坑
JWT 过期时间的判定是严格按时间戳来的。如果你在生成 token 时使用expires=timedelta(seconds=1),那么它实际上只在一秒内有效。测试时经常遇到“刚签发就过期”的问题,这通常是因为你设置了极短的过期时间,但签发和校验之间有网络延迟。
另一个相关参数是JWT_LEEWAY,它允许在 token 过期后还有一定宽限期。比如你设置JWT_LEEWAY = timedelta(seconds=30),那么 token 过期后 30 秒内仍然通过校验。这个参数主要用于容忍客户端与服务器之间的时钟偏差,正常情况下不建议设置过大,否则“过期失效”就失去意义了。
还有一个容易忽略的场景:前端本地时间不准。JWT 的 exp 是 UTC 时间戳,如果用户设备的时钟慢了,前端可能提前认为 token 过期。但我建议前端不要自己解析 token 来判断过期时间,而是依赖接口响应。当接口返回 401 时,再去刷新 token 或重新登录。这样更可靠。
4.4 黑名单功能开了却没用?Redis 存储与回调函数
很多人在配置里设置了JWT_BLACKLIST_ENABLED = True,却发现 token 注销后依旧能用。原因只有一个:没有实现token_in_blocklist_loader回调。
扩展本身不会知道你打算怎么存储黑名单,它只是调用你提供的回调来询问“这个 token 在黑名单里吗?”如果你不实现,扩展就默认不在黑名单里,于是怎么注销都没用。所以必须写回调,并在回调里查询你的存储介质。
查询存储介质时,要注意性能。如果用数据库,每次请求都查一次黑名单表,可能成为瓶颈。token 校验本身是纯内存操作,突然加一次数据库查询,高并发下压力不小。这也是很多项目选择把黑名单放到 Redis 的原因。
以下是基于 Redis 的示例:
import redis redis_client = redis.Redis(host="localhost", port=6379, db=0) @jwt.token_in_blocklist_loader def check_if_token_revoked(jwt_header, jwt_payload): jti = jwt_payload["jti"] return redis_client.exists(f"blacklist:{jti}")注销时,将 jti 以 token 剩余有效期作为 TTL 写入 Redis:
@app.delete("/logout") @jwt_required() def logout(): jti = get_jwt()["jti"] exp = get_jwt()["exp"] now = datetime.now(timezone.utc) ttl = exp - int(now.timestamp()) redis_client.setex(f"blacklist:{jti}", ttl, "true") return {"msg": "已注销"}, 200注意要先import time并考虑时区问题。这里exp是 UTC 时间戳,now.timestamp()需要是 UTC 的,所以最好用datetime.now(timezone.utc).timestamp()。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 不带 token 也能访问接口 | 装饰器顺序错误 / JWTManager 未初始化 | 调整装饰器顺序,确保初始化 |
| Authorization 头有 token 但 401 | 缺少 Bearer 前缀 | 在 token 前添加 "Bearer " |
| refresh token 无法访问业务接口 | 用 @jwt_required() 校验 refresh token | 业务接口用 access token,刷新接口用 refresh=True |
| token 刚签发就过期 | 设置了太短的 expires 或时钟偏差 | 调长过期时间,设置 JWT_LEEWAY |
| 黑名单不生效 | 未实现 token_in_blocklist_loader | 实现回调并开启黑名单配置 |
| get_jwt_identity() 返回 None | 在未受保护的接口里调用,或 token 无效 | 使用 @jwt_required() 保护后再调用 |
| 修改密码后旧 token 仍有效 | 未引入黑名单或 token 版本机制 | 为每个用户维护 token 版本,在密码修改后使其失效 |
5. 进阶实践:让认证系统更健壮
5.1 refresh token 轮换与重放检测
上面我们提到刷新时要把旧 refresh token 加入黑名单,这种策略能有效防止重放攻击。设想一下:攻击者截获了你的 refresh token,在你不知情的情况下尝试刷新;而你自己也会在某个时间点刷新,这样就会有两个 refresh token 在轮流使用。如果服务器发现同一个用户连续出现了不同的 refresh token jti,就可以判定发生了重放。
更简单的做法是“刷新即失效”,也就是每次刷新后,旧的 refresh token 立即作废。这样攻击者即使拿到了旧的 refresh token,也必须在刷新前使用,时间窗口很短。我在生产环境里通常还会在刷新时检查该用户当前持有的 refresh token 是否和签发时记录的一致,如果发现不一致,直接把该用户的所有 token 都拉黑并要求重新登录。
5.2 多用户角色权限结合
JWT 的 payload 可以自定义字段,除了 identity 之外,还可以把角色、权限编码进去。比如:
access_token = create_access_token(identity=str(user.id), additional_claims={"role": user.role})之后在视图里通过get_jwt()访问这些自定义声明:
from flask_jwt_extended import get_jwt @app.get("/admin") @jwt_required() def admin_only(): claims = get_jwt() if claims.get("role") != "admin": return {"msg": "权限不足"}, 403 return {"msg": "欢迎管理员"}这样做的优点是权限判断不需要查数据库,直接解析 token 就能完成,性能更好。缺点是如果用户角色变了,旧的 token 里还是老角色,只有等 token 过期才能生效。所以对权限变化要求严格的系统,最好在每次请求时从数据库读取最新角色,或者使用短期 access token(比如 5 分钟过期)来降低延迟。
5.3 统一异常处理与错误响应格式
默认情况下,Flask-JWT-Extended 返回的错误响应是一个 JSON 字典,比如{"msg": "Missing Authorization Header"}。实际项目中我们可能希望统一所有错误格式,比如{"code": 401, "message": "未授权", "data": null}。可以通过扩展提供的错误处理器来定制。
@jwt.unauthorized_loader def missing_token_callback(reason): return jsonify(code=401, message="缺少 token", data=None), 401 @jwt.invalid_token_loader def invalid_token_callback(reason): return jsonify(code=401, message="无效 token", data=None), 401 @jwt.expired_token_loader def expired_token_callback(jwt_header, jwt_payload): return jsonify(code=401, message="token 已过期", data=None), 401 @jwt.revoked_token_loader def revoked_token_callback(jwt_header, jwt_payload): return jsonify(code=401, message="token 已失效", data=None), 4015.4 与 Swagger/OpenAPI 文档集成
调试 JWT 接口时,Swagger 授权功能很实用。如果你用 flasgger 或 flask-restx,可以配置全局 authorization 参数。这里以 flask-restx 为例:
authorizations = { "Bearer": { "type": "apiKey", "in": "header", "name": "Authorization", "description": "形如 Bearer <token>" } } api = Api(app, authorizations=authorizations)然后在需要鉴权的接口标注@api.doc(security="Bearer"),Swagger UI 上就会出现“Authorize”按钮。
6. 写在最后的一些经验
如果你准备在自己的项目里接入 JWT,我建议不要把时间花在纠结“自己写还是用现成库”上。除非你的需求真的非常简单,否则直接用 Flask-JWT-Extended,把精力放到业务本身。
在生产部署时,有几个细节值得反复确认:JWT_SECRET_KEY 是否来自环境变量而不是代码库;access token 过期间隔是否调短到 15~30 分钟;刷新接口是否有速率限制;日志里是否可能打印出完整的 Authorization 头。这些都是安全上的红线。
另外,我个人习惯给 refresh token 加上用户代理和 IP 等额外声明,在刷新时做校验,比如:
refresh_token = create_refresh_token( identity=str(user.id), additional_claims={"user_agent": request.headers.get("User-Agent", "")[:200]} )不过这只能作为辅助手段,毕竟用户代理和 IP 都可能变化,不能作为强校验依据。
最后说一个不少团队忽略的点:前端对 token 过期必须要有全局处理方案。我见过很多项目在后端都写对了,但前端没有拦截 401,导致用户明明登录了,操作到一半却跳回登录页。比较好的做法是在 axios 响应拦截器里统一判断 401,然后尝试静默刷新 token,刷新成功就重放请求,刷新失败才跳转登录页。这套流程和后端的 token 刷新接口配合起来,用户体验会顺畅很多。
希望这篇解析能帮你把 Flask-JWT-Extended 用得更顺手。如果你在实施过程中遇到了别的坑,欢迎一起交流。