localhost HTTPS环境下Flask后端无法接收HttpOnly认证Cookie
问题诊断
核心原因是前端通过file://协议直接打开本地HTML文件,而后端运行在https://localhost:5000,两者属于不同的站点上下文。浏览器的Cookie安全机制(尤其是分区Cookie规范)会阻止非分区的HttpOnly Cookie在这种跨站点请求中被携带——这就是API客户端(Bruno)能正常工作,但浏览器不行的原因,因为API客户端不受浏览器的同源/跨站点Cookie限制。
解决方案
有两种可行的解决方式:
方式一:将前端托管到HTTP服务器(推荐)
不要直接打开本地HTML文件,而是用HTTP服务器托管前端,让前端和后端处于同站点上下文:
- 把前端HTML文件放到Flask项目的
static目录下 - 修改Flask代码,添加路由托管前端:
from flask import send_from_directory @app.route('/frontend') def serve_frontend(): return send_from_directory('static', 'index.html')
- 通过
https://localhost:5000/frontend访问前端,此时前后端同域,Cookie会正常携带。
或者用Python自带的HTTP服务器临时托管:
# 在前端文件所在目录执行 python -m http.server 8000 --bind localhost
然后通过https://localhost:8000/index.html访问前端,同时需要在Flask的CORS配置中添加允许的源:
CORS(app, supports_credentials=True, origins=["https://localhost:8000"])
方式二:启用Cookie分区(适用于跨站点场景)
如果必须保持前端和后端跨站点,可以在设置Cookie时添加partitioned=True属性(需要Chrome 104+/Firefox 115+以上版本浏览器支持):
修改Flask登录接口的Cookie设置:
response.set_cookie( 'auth', token, httponly=True, samesite='None', secure=True, partitioned=True # 新增这一行 )
这样浏览器会将该Cookie标记为可跨站点携带,即使前端是file://或其他站点,也能在请求后端时带上。
验证效果
修改后重新启动后端,执行登录请求后,查看Cookie信息会多出Partitioned: True标记,后续的/api/user请求就会正常携带auth Cookie。
原始问题信息
问题描述
无法将HttpOnly认证Cookie发送至Flask后端。已添加secure标记、将后端转为HTTPS、在前端设置credentials: 'include'、启用支持凭证的CORS等,但后端仍未收到Cookie。执行login请求可成功设置auth Cookie,但后续user请求未携带该Cookie,后端返回403(提示Token缺失)。该流程在API客户端(Bruno)中可正常运行,但在浏览器中失败。
后端代码(使用mkcert生成证书,HTTPS访问根路径正常)
from flask import Flask, request, jsonify import jwt from flask_cors import CORS app = Flask(__name__) app.config['SECRET_KEY'] = 'your_secret_key' CORS(app, supports_credentials=True) @app.route('/api/user', methods=['GET']) def get_user(): token = request.cookies.get('auth') if not token: return jsonify({'message': 'Token is missing!'}), 403 try: data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"]) username = data.get('username') email = data.get('email') return jsonify({'username': username, 'email': email}) except jwt.ExpiredSignatureError: return jsonify({'message': 'Token has expired!'}), 403 except jwt.InvalidTokenError: return jsonify({'message': 'Invalid token!'}), 403 @app.route('/') def index(): return '<html><body><h1>Hello, World!</h1></body></html>' @app.route('/api/login', methods=['POST']) def login(): data = request.get_json() username = data.get('username') email = data.get('email') if not username or not email: return jsonify({'message': 'Username and email are required!'}), 400 token = jwt.encode({'username': username, 'email': email}, app.config['SECRET_KEY'], algorithm="HS256") response = jsonify({'message': 'Login successful!'}) response.set_cookie('auth', token, httponly=True, samesite='None', secure=True) return response if __name__ == '__main__': app.run(debug=True, ssl_context=('/home/ashkan/Desktop/spring 25/spring 25 sideprojects/auth-mre-flask/backend/localhost.pem', '/home/ashkan/Desktop/spring 25/spring 25 sideprojects/auth-mre-flask/backend/localhost-key.pem'))
前端代码(直接在浏览器打开的单文件)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Basic HTML Page</title> </head> <body> <h1>check the console</h1> <script> (async () => { console.log("logging in...") const loginResponse = await fetch('https://localhost:5000/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ "username": "flask-test", "email": "a@a.com" }) }) const loginData = await loginResponse.json(); console.log(loginData) if (!loginResponse.ok) { console.log('login failed :(') return } console.log("getting user info...") const userResponse = await fetch('https://localhost:5000/api/user', { credentials: 'include' }); const userData = await userResponse.json(); console.log(userData) if (!userResponse.ok) { console.log("user data fetch failed :(") } })(); </script> </body> </html>
浏览器控制台日志
index.html:14 logging in... index.html:27 {message: 'Login successful!'} index.html:34 getting user info... index.html:36 GET https://localhost:5000/api/user 403 (FORBIDDEN) (anonymous) @ index.html:36 await in (anonymous) (anonymous) @ index.html:44 index.html:40 {message: 'Token is missing!'} index.html:42 user data fetch failed :(
Cookie信息
- login请求接收的Cookie:
Name: auth Value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... Domain: localhost Path: / Expires / Max-Age: Session Size: 185 HttpOnly: True Secure: True SameSite: None Partition Key Site: (nothing) Cross Site: (nothing) Priority: Medium
- user请求发送的Cookie(未包含auth):
Name: csrftoken Value: 6Q8P4108ffbgVCsmF5AVBY5PxCeHp14A... Domain: localhost Path: / Expires / Max-Age: 2025-07-17T20:50:27.576Z Size: 41 HttpOnly: False Secure: True SameSite: None Partition Key Site: (nothing) Cross Site: (nothing) Priority: Medium
内容的提问来源于stack exchange,提问作者Ashkan Arabi

