
做后端接口开发绕不开认证授权这件事。早期做 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 和第三方开发者接入。JWTJSON 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(identityuser_id)签发 refresh token 用create_refresh_token(identityuser_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(usernamerequest.json[username]).first() if user and user.check_password(request.json[password]): access_token create_access_token(identitystr(user.id)) refresh_token create_refresh_token(identitystr(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 tokenFlask-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(idarticle_id, user_iduser_id).first() else: article Article.query.get(article_id) return ...还有一种是 fresh token。有些安全敏感操作比如修改密码、绑定手机号你希望用户必须刚登录过而不是用一个刷新了好几次的长寿 token。这时你可以给 access token 加一个freshTrue参数然后视图用jwt_required(freshTrue)装饰。假如用户 token 不新鲜就得重新登录再操作。这个机制很实用能有效防止 token 在长时间有效期内被滥用。access_token create_access_token(identitystr(user.id), freshTrue) refresh_token create_refresh_token(identitystr(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(minutes30)refresh token 可以设置成datetime.timedelta(days7)。这里要特别注意如果你使用expirestimedelta(...)来单独指定某个 token 的过期时间那么它会覆盖全局配置。from datetime import timedelta app.config[JWT_SECRET_KEY] super-secret-key-change-me app.config[JWT_ACCESS_TOKEN_EXPIRES] timedelta(minutes30) app.config[JWT_REFRESH_TOKEN_EXPIRES] timedelta(days7)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(minutes30) app.config[JWT_REFRESH_TOKEN_EXPIRES] timedelta(days7) 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_keyTrue) username db.Column(db.String(80), uniqueTrue, nullableFalse) password_hash db.Column(db.String(128), nullableFalse) is_active db.Column(db.Boolean, defaultTrue) 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(usernameusername).first(): return {msg: 用户名已存在}, 409 user User(usernameusername) 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(usernameusername).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(identitystr(user.id), freshTrue) refresh_token create_refresh_token(identitystr(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(refreshTrue)这个变体专门用来校验 refresh token。在刷新逻辑里我们拿到当前 refresh token 的 jtitoken 的唯一 ID先把它加入黑名单再签发一组全新的 token。这就是“refresh token 轮换”每次刷新旧 refresh token 立即失效即使泄露了也无法再次使用。要为黑名单功能写回调。我们需要一个 TokenBlocklist 模型来存储被拉黑的 jti 和过期时间。其实也可以用 Redis但为了演示简单我用数据库表。class TokenBlocklist(db.Model): id db.Column(db.Integer, primary_keyTrue) jti db.Column(db.String(36), nullableFalse, indexTrue) token_type db.Column(db.String(10), nullableFalse) user_id db.Column(db.String(36), nullableFalse) created_at db.Column(db.DateTime, nullableFalse) expires_at db.Column(db.DateTime, nullableFalse)然后实现黑名单回调jwt.token_in_blocklist_loader def check_if_token_revoked(jwt_header, jwt_payload): jti jwt_payload[jti] token TokenBlocklist.query.filter_by(jtijti).first() return token is not None这个回调会在每次校验 token 时被调用。如果返回 True说明 token 在黑名单里请求会被拒绝。刷新接口和注销接口实现如下app.post(/refresh) jwt_required(refreshTrue) def refresh(): identity get_jwt_identity() access_token create_access_token(identityidentity, freshFalse) return {access_token: access_token, expires_in: 1800}看到没有这里我签发的新 access token 的freshFalse它表示这个 token 不是最近登录获得的而是通过刷新得到的。如果某个高敏感操作要求 freshTrue用户就需要重新登录。那么“注销登录”该怎么实现呢核心思路是把当前使用的 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( jtijti, token_typeaccess, user_idget_jwt_identity(), created_atnow, expires_atnow timedelta(minutes30) ) db.session.add(token_block) db.session.commit() return {msg: 已注销}, 200如果你要在刷新时同时注销旧 refresh token可以再写一个jwt_required(refreshTrue)的接口在签发新 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 时使用expirestimedelta(seconds1)那么它实际上只在一秒内有效。测试时经常遇到“刚签发就过期”的问题这通常是因为你设置了极短的过期时间但签发和校验之间有网络延迟。另一个相关参数是JWT_LEEWAY它允许在 token 过期后还有一定宽限期。比如你设置JWT_LEEWAY timedelta(seconds30)那么 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(hostlocalhost, port6379, db0) jwt.token_in_blocklist_loader def check_if_token_revoked(jwt_header, jwt_payload): jti jwt_payload[jti] return redis_client.exists(fblacklist:{jti})注销时将 jti 以 token 剩余有效期作为 TTL 写入 Redisapp.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(fblacklist:{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刷新接口用 refreshTruetoken 刚签发就过期设置了太短的 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(identitystr(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(code401, message缺少 token, dataNone), 401 jwt.invalid_token_loader def invalid_token_callback(reason): return jsonify(code401, message无效 token, dataNone), 401 jwt.expired_token_loader def expired_token_callback(jwt_header, jwt_payload): return jsonify(code401, messagetoken 已过期, dataNone), 401 jwt.revoked_token_loader def revoked_token_callback(jwt_header, jwt_payload): return jsonify(code401, messagetoken 已失效, dataNone), 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, authorizationsauthorizations)然后在需要鉴权的接口标注api.doc(securityBearer)Swagger UI 上就会出现“Authorize”按钮。6. 写在最后的一些经验如果你准备在自己的项目里接入 JWT我建议不要把时间花在纠结“自己写还是用现成库”上。除非你的需求真的非常简单否则直接用 Flask-JWT-Extended把精力放到业务本身。在生产部署时有几个细节值得反复确认JWT_SECRET_KEY 是否来自环境变量而不是代码库access token 过期间隔是否调短到 15~30 分钟刷新接口是否有速率限制日志里是否可能打印出完整的 Authorization 头。这些都是安全上的红线。另外我个人习惯给 refresh token 加上用户代理和 IP 等额外声明在刷新时做校验比如refresh_token create_refresh_token( identitystr(user.id), additional_claims{user_agent: request.headers.get(User-Agent, )[:200]} )不过这只能作为辅助手段毕竟用户代理和 IP 都可能变化不能作为强校验依据。最后说一个不少团队忽略的点前端对 token 过期必须要有全局处理方案。我见过很多项目在后端都写对了但前端没有拦截 401导致用户明明登录了操作到一半却跳回登录页。比较好的做法是在 axios 响应拦截器里统一判断 401然后尝试静默刷新 token刷新成功就重放请求刷新失败才跳转登录页。这套流程和后端的 token 刷新接口配合起来用户体验会顺畅很多。希望这篇解析能帮你把 Flask-JWT-Extended 用得更顺手。如果你在实施过程中遇到了别的坑欢迎一起交流。