PAYATHON 2026

Odoo 11 SameSite Cookie 在支付回跳后被清空

支付阿杰

明确结论

不要直接把 Odoo 11 使用的 Werkzeug 升级到最新版。更稳妥的处理方式是为 Odoo 的 session_id Cookie 添加以下属性,并确保站点始终使用 HTTPS:

SameSite=None; Secure

如果支付平台支持调整回跳方式,优先采用顶层页面的 GET 跳转,并通过服务端 webhook 确认支付结果。只有支付平台必须通过跨站 POST、iframe 等方式返回时,通常才需要设置 SameSite=None。

Chrome 中显示“Cookie 为空”,并不一定代表 Cookie 已被删除。更常见的情况是,浏览器受 SameSite 策略限制,没有在这次跨站请求中发送 Cookie。Odoo 随后创建了新 session,看起来就像原会话被清空了。

原因

旧版 Odoo 11 和 Werkzeug 创建 session Cookie 时,通常不会显式设置 SameSite。新版 Chrome 会把未声明 SameSite 的 Cookie 当作 SameSite=Lax 处理。

不同请求方式的表现如下:

  • 跨站顶层 GET 导航通常会携带 SameSite=Lax Cookie。
  • 从支付域名跨站 POST 回 Odoo 时,通常不会携带该 Cookie。
  • iframe 和第三方上下文受到的限制更严格。
  • Cookie 设置为 SameSite=None 时,必须同时设置 Secure,否则新版 Chrome 会拒绝该 Cookie。
  • Secure Cookie 只能通过 HTTPS 正常发送。

因此,支付平台通过跨站 POST 返回时,Odoo 可能收不到原来的 session_id,并把该请求识别为新会话。

如果 Odoo 前面部署了 Nginx,建议优先在代理层处理。这样既不需要修改旧版 Werkzeug,也能避开升级依赖可能造成的兼容问题。

支持 proxy_cookie_flags 的 Nginx 可以使用以下配置:

location / {
    proxy_pass http://odoo_upstream;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    proxy_cookie_flags session_id secure httponly samesite=none;
}

重新加载 Nginx 后,Odoo 返回的响应头应该类似:

Set-Cookie: session_id=...; Path=/; HttpOnly; Secure; SameSite=None

如果当前 Nginx 不支持 proxy_cookie_flags,也可以通过 proxy_cookie_path 追加属性。不过,这种方式会影响所有匹配到的 Cookie,使用前需要确认站点中各 Cookie 的作用范围:

location / {
    proxy_pass http://odoo_upstream;

    proxy_cookie_path / "/; Secure; HttpOnly; SameSite=None";
}

修改后先检查配置,再重新加载 Nginx:

nginx -t
systemctl reload nginx

实际服务名称可能随操作系统和部署方式而不同。

应用层兼容方案

如果无法在反向代理层修改响应头,可以等 Odoo 生成响应后、浏览器接收响应前,对 Set-Cookie 响应头进行兼容处理。

下面的代码不要求旧版 Werkzeug 支持 samesite 参数:

def patch_session_cookie(response, cookie_name='session_id'):
    set_cookie_headers = response.headers.getlist('Set-Cookie')
    if not set_cookie_headers:
        return response

    del response.headers['Set-Cookie']
    cookie_prefix = cookie_name + '='

    for value in set_cookie_headers:
        if value.startswith(cookie_prefix):
            lower_value = value.lower()

            if 'samesite=' not in lower_value:
                value += '; SameSite=None'

            if 'secure' not in lower_value:
                value += '; Secure'

        response.headers.add('Set-Cookie', value)

    return response

这段代码必须在 Odoo 和 Werkzeug 完成所有 response.set_cookie(...) 操作之后、WSGI 发送响应之前调用:

response = patch_session_cookie(response)
return response

除非已经确认当前 Werkzeug 的 Response.set_cookie() 支持 samesite 参数,否则不要使用下面的写法:

response.set_cookie(
    'session_id',
    session_id,
    httponly=True,
    secure=True,
    samesite='None',
)

旧版 Werkzeug 可能无法识别 samesite 参数,并直接抛出 TypeError。生产环境中,最好将兼容逻辑放进 WSGI 中间件或可维护的补丁,避免临时修改在后续部署或升级时丢失。

更合适的支付回跳设计

如果支付平台允许配置,可以把支付流程拆成两条链路:

  1. 支付平台通过服务端 webhook 将支付结果通知 Odoo。
  2. 用户浏览器通过顶层 GET 跳转回订单结果页。
  3. Odoo 根据订单号或交易号查询服务端已经确认的支付状态。
  4. 浏览器回跳参数只负责定位订单,不能作为支付成功的唯一依据。

例如:

GET /payment/return?transaction_ref=TX123

顶层跨站 GET 通常可以配合 SameSite=Lax 使用,其安全边界也比全站设置 SameSite=None 更清楚。

无论采用哪种方式,都不能只根据浏览器的回跳参数把订单标记为已支付。必须验证支付平台签名,或者通过服务端 webhook、支付查询接口确认结果。

排查与验证

先打开 Chrome DevTools 的 Network 面板,检查支付回跳请求:

  • Request Headers 中有没有 Cookie: session_id=...。
  • 回跳响应是否返回了新的 Set-Cookie。
  • Set-Cookie 是否包含 SameSite=None; Secure。
  • Console 或 Issues 面板是否提示 Cookie 因 SameSite 或 Secure 属性被阻止。
  • 回跳 URL 是否使用同一主机名,例如有没有在 www.example.com 和 example.com 之间切换。
  • HTTP 与 HTTPS、Cookie 的 Domain 和 Path 是否一致。

以下命令可以检查响应头,但只能确认服务器如何设置 Cookie,无法完整模拟浏览器的 SameSite 行为:

curl -I https://odoo.example.com/web/login

正常情况下,响应中应该包含类似内容:

Set-Cookie: session_id=...; Path=/; HttpOnly; Secure; SameSite=None

配置完成后,先清除测试浏览器中的旧 Cookie,重新登录,再完整执行一次支付流程。服务器配置发生变化后,已有 Cookie 不会自动获得新属性,必须等服务器重新发送 Set-Cookie。

注意事项

  • Odoo 必须通过 HTTPS 对外提供服务,否则 SameSite=None; Secure Cookie 无法正常工作。
  • 如果由反向代理终止 TLS,需要正确传递 X-Forwarded-Proto,并确认 Odoo 已正确配置代理模式。
  • 不建议只为一个 Cookie 参数就把 Werkzeug 升级到跨度很大的版本。Odoo 11 可能依赖旧版 API,应先在测试环境验证兼容性。
  • 不要无条件把所有 Cookie 都改成 SameSite=None。最好只处理 session_id,或者其他确实需要跨站发送的 Cookie。
  • SameSite=None 会扩大 Cookie 在跨站请求中的发送范围。涉及状态修改的路由仍然需要保留 CSRF、防重放和支付签名校验。
  • 如果支付页在 iframe 中运行,浏览器的第三方 Cookie 限制仍可能阻止 Cookie。SameSite=None 无法绕过用户或浏览器禁用第三方 Cookie 的策略。
  • 如果支付平台使用的是顶层 GET 回跳,但 Cookie 仍未发送,应继续检查 Domain、Path、HTTPS、主机名变化、Cookie 是否过期,以及响应中是否存在用于删除 Cookie 的 Set-Cookie,不要只排查 SameSite。

备注:内容仅供参考。