You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

localhost HTTPS环境下Flask后端无法接收HttpOnly认证Cookie

解决HttpOnly认证Cookie无法发送至Flask后端的问题

问题诊断

核心原因是前端通过file://协议直接打开本地HTML文件,而后端运行在https://localhost:5000,两者属于不同的站点上下文。浏览器的Cookie安全机制(尤其是分区Cookie规范)会阻止非分区的HttpOnly Cookie在这种跨站点请求中被携带——这就是API客户端(Bruno)能正常工作,但浏览器不行的原因,因为API客户端不受浏览器的同源/跨站点Cookie限制。

解决方案

有两种可行的解决方式:

方式一:将前端托管到HTTP服务器(推荐)

不要直接打开本地HTML文件,而是用HTTP服务器托管前端,让前端和后端处于同站点上下文:

  1. 把前端HTML文件放到Flask项目的static目录下
  2. 修改Flask代码,添加路由托管前端:
from flask import send_from_directory

@app.route('/frontend')
def serve_frontend():
    return send_from_directory('static', 'index.html')
  1. 通过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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 18:05:54