diff --git a/app/core/security.py b/app/core/security.py index adc9a328..1ac1594d 100644 --- a/app/core/security.py +++ b/app/core/security.py @@ -188,12 +188,19 @@ def set_or_refresh_resource_token_cookie( purpose="resource" ) + # 判断请求是否为 HTTPS:直连协议为 https,或经反向代理转发时携带 X-Forwarded-Proto: https。 + # 无法确认为明文 HTTP 时按 fail-safe 默认设置 secure=True,避免代理终止 HTTPS 后以 HTTP 转发导致 Cookie 明文传输。 + is_https = ( + request.url.scheme == "https" + or request.headers.get("x-forwarded-proto", "").lower() == "https" + ) + # 设置会话级别的 HttpOnly Cookie response.set_cookie( key=settings.PROJECT_NAME, value=resource_token, httponly=True, - secure=request.url.scheme == "https", # 根据当前请求的协议设置 secure 属性 + secure=is_https, # 根据当前请求协议(含反向代理转发标识)设置 secure 属性 samesite="lax" # 不同浏览器对 "Strict" 的处理可能不同,设置 SameSite 为 "Lax",以平衡安全性和兼容性 ) diff --git a/tests/test_resource_token_cookie_secure_flag.py b/tests/test_resource_token_cookie_secure_flag.py new file mode 100644 index 00000000..48c49de3 --- /dev/null +++ b/tests/test_resource_token_cookie_secure_flag.py @@ -0,0 +1,38 @@ +from unittest import TestCase + +from fastapi import Response + +from app import schemas +from app.core.security import set_or_refresh_resource_token_cookie + + +class FakeURL: + def __init__(self, scheme: str) -> None: + self.scheme = scheme + + +class FakeRequest: + """ + 最小化的请求桩对象,仅提供 set_or_refresh_resource_token_cookie 所需属性。 + """ + + def __init__(self, scheme: str, headers: dict | None = None) -> None: + self.url = FakeURL(scheme) + self.headers = headers or {} + self.cookies: dict = {} + + +class ResourceTokenCookieSecureFlagTest(TestCase): + def test_secure_flag_set_when_https_terminated_at_reverse_proxy(self): + """ + 当反向代理(如 nginx)终止 HTTPS 并以 HTTP 转发给后端时, + 资源令牌 Cookie 仍必须携带 secure 属性,不能因为直连请求协议是 http 就降级。 + """ + request = FakeRequest(scheme="http", headers={"x-forwarded-proto": "https"}) + response = Response() + payload = schemas.TokenPayload(sub=1, username="test", super_user=False, level=1) + + set_or_refresh_resource_token_cookie(request, response, payload) + + set_cookie_header = response.headers.get("set-cookie", "") + self.assertIn("Secure", set_cookie_header)