Flask WTForms与JWT集成:POST请求CSRF令牌缺失问题求助
核心冲突点
Flask-WTF 和 Flask-JWT-Extended 维护两套独立的 CSRF 保护机制:
- WTForms 的 CSRF 令牌存储在 session 中,通过表单的
csrf_token字段验证 - Flask-JWT-Extended 的 CSRF 令牌(
csrf_access_token)存储在 Cookie 中,针对受保护的 POST/PUT/DELETE 请求,会强制验证该令牌
你的受保护 POST 路由同时触发了 JWT 的 CSRF 检查,但表单提交时只携带了 WTForms 的 CSRF 令牌,未提供 JWT 要求的令牌,因此抛出错误。
调试步骤
检查请求数据
在受保护的 POST 路由中添加打印语句,确认请求是否携带了 JWT 所需的 CSRF 令牌:@jwt_required_and_not_revoked def post(self): print("Request Headers:", request.headers) print("Request Form Data:", request.form) form = SolarSearchForm() # ... 后续代码重点查看是否存在
X-CSRF-TOKEN请求头,或表单中是否有csrf_access_token字段。验证 JWT 配置
检查 Flask 配置中与 JWT CSRF 相关的参数:app.config["JWT_COOKIE_CSRF_PROTECT"] = True # 该配置会强制对POST请求检查CSRF app.config["JWT_CSRF_IN_COOKIES"] = True app.config["JWT_ACCESS_CSRF_HEADER_NAME"] = "X-CSRF-TOKEN" # JWT默认检查的请求头若
JWT_COOKIE_CSRF_PROTECT为True,则必须提供对应的 CSRF 令牌。检查登录流程的 Cookie 设置
你当前手动设置access_token_cookie,但未使用 Flask-JWT-Extended 提供的工具方法,可能导致csrf_access_token与access_token不关联:
替换登录路由中的 Cookie 设置代码:from flask_jwt_extended import set_access_cookies, set_refresh_cookies # 原代码: # response.set_cookie('access_token_cookie', access_token) # response.set_cookie('refresh_token_cookie', refresh_token) # 替换为: set_access_cookies(response, access_token) set_refresh_cookies(response, refresh_token)该方法会自动生成并设置匹配的
csrf_access_tokenCookie,确保令牌关联正确。
解决方案
方案1:同时提交两套 CSRF 令牌
在表单模板中添加 JWT CSRF 令牌的隐藏字段,同时提交 WTForms 和 JWT 的令牌:
<form method="POST"> {{ form.hidden_tag() }} <!-- WTForms的CSRF令牌 --> <input type="hidden" name="csrf_access_token" value="{{ request.cookies.get('csrf_access_token') }}"> <!-- 其他表单字段 --> <button type="submit">提交</button> </form>
或者通过 AJAX 提交时,在请求头中携带 JWT CSRF 令牌:
const csrfToken = document.cookie.split('csrf_access_token=')[1].split(';')[0]; fetch('/your-protected-route', { method: 'POST', headers: { 'X-CSRF-TOKEN': csrfToken, 'Content-Type': 'application/x-www-form-urlencoded' }, body: new FormData(form) });
方案2:关闭特定路由的 JWT CSRF 检查
如果已通过 WTForms 实现 CSRF 保护,不需要重复验证,可以在路由装饰器中关闭 JWT 的 CSRF 检查(需确认 Flask-JWT-Extended 版本,新版本参数为 protect_csrf=False):
@jwt_required_and_not_revoked(protect_csrf=False) def post(self): form = SolarSearchForm() # ... 后续代码
内容的提问来源于stack exchange,提问作者itsPav

