如何从一个域重定向到另一个域并为另一个域设置 cookie 或标头?

use*_*837 10 python cookies redirect cross-domain fastapi

我正在使用 FastAPIRedirectResponse并尝试将用户从一个应用程序(域)重定向到另一个应用程序(域),并在 ; 中设置一些responsecookie 然而,cookie 总是被删除/不被传输。如果我尝试添加一些标头,我添加到的所有标头RedirectResponse也不会传输。

@router.post("/callback")
async def sso_callback(request: Request):
   jwt_token = generate_token(request)
   redirect_response = RedirectResponse(url="http://192.168.10.1/app/callback", 
                             status_code=303)
   redirect_response.set_cookie(key="accessToken", value=jwt_token, httponly=True)
   redirect_response.headers["Authorization"] = str(jwt_token)
   return redirect_response
Run Code Online (Sandbox Code Playgroud)

我该如何解决这个问题?先谢谢您的帮助。

Chr*_*ris 20

如此处所述,无论您使用哪种语言或框架,都无法重定向到设置了自定义标头的另一个域。HTTP 协议中的重定向基本上是Location与响应关联的标头(即 ),并且不允许添加任何指向目标位置的标头。Authorization当您在示例中添加标头时,您基本上是为指示浏览器重定向的响应设置该标头,而不是为重定向本身设置标头。换句话说,您将该标头发送回客户端

\n

至于HTTP cookie,浏览器将服务器发送的 cookie 与响应一起存储(使用Set-Cookie标头),然后将 cookie 与向同一服务器发出的请求一起发送到 HTTP 标头内Cookie。根据文档

\n
\n

HTTPSet-Cookie响应标头用于将 cookie 从服务器发送到用户代理,以便用户代理稍后可以将其发送回服务器。要发送多个 cookie,Set-Cookie应在同一个响应中发送多个 \n标头。

\n
\n

因此,如果这是从一个应用程序(具有子域,例如abc.example.test)到另一个应用程序(具有子域,例如xyz.example.test)的重定向,并且两个应用程序都具有相同的(父)域(并且在创建 cookie 时domain将标志设置为example.test),cookie 将在两个应用程序之间成功共享(就像指定domain的那样,则始终包含子域)。浏览器将使 cookie 可用于给定域(包括任何子域),无论使用哪种协议 (HTTP/HTTPS) 或端口。domain您可以使用和标志限制 cookie 的可用性path,也可以使用和标志限制对 cookie 的访问(请参阅此处此处,以及Starlette 文档)。如果未设置该标志,潜在的攻击者可以通过 JavaScript (JS) 读取和修改信息,而带有securehttpOnlyhttpOnlyhttpOnly属性的cookie只会发送到服务器,而客户端的JS无法访问。

\n

但是,不能为不同的域设置 cookie。如果允许这样做,将会带来巨大的安全缺陷。因此,由于您“尝试使用某些 cookie 设置将用户从一个应用程序(域)重定向到另一个应用程序(域)...”,因此它不会工作,因为 cookie 只会与向同一发出的请求一起发送。

\n

解决方案1

\n

如此处所述,解决方案是让域(应用程序)A 将用户重定向到域(应用程序)B,并将access-tokenURL 作为查询参数传递。然后,域 B 将读取令牌并设置自己的 cookie,以便浏览器将存储该 cookie 并将其与每个后续请求一起发送到域 B。

\n

请注意,您应该考虑使用安全 (HTTPS) 通信,以便加密传输令牌,并secure在创建 cookie 时设置标志。另请注意,在查询字符串 中包含令牌会带来严重的安全风险,因为敏感数据不应在查询字符串中传递。这是因为作为 URL 一部分的查询字符串出现在浏览器的地址栏中;因此,允许用户查看带有令牌的 URL 并为其添加书签(意味着它保存在磁盘上)。此外,URL 还将进入浏览历史记录,这意味着它无论如何都会被写入磁盘并出现在选项History卡中(按Ctrl+H可查看浏览器的历史记录)。上述两种情况都会允许攻击者(以及与您共享计算机/移动设备的人)窃取此类敏感数据。此外,许多浏览器插件/扩展程序会跟踪用户的浏览活动\xe2\x80\x94您访问的每个 URL 都会发送到他们的服务器进行分析,以便检测恶意网站并提前警告您。因此,在使用下面的方法之前,您应该考虑上述所有内容(有关此主题的相关帖子,请参阅此处此处此处)。

\n

为了防止在地址栏中显示 URL,下面的方法也在域 B 内使用重定向。一旦域 B 收到以令牌作为查询参数的路由请求/submit,域 B 将重定向到其中不包含令牌的裸 URL(即其home页面)进行响应。由于此重定向,带有令牌的 URL 最终不会出现在浏览历史记录中。虽然这提供了一些针对前面描述的某些攻击的保护,但这并不意味着浏览器扩展等仍然无法捕获带有令牌的 URL。

\n

如果您在本地主机上进行测试,则需要为应用程序 B 提供一个不同的域名;否则,如前所述,cookie 将在具有相同域的应用程序之间共享,因此,您最终将收到为域 A 设置的 cookie,并且无法判断该方法是否有效。为此,您必须编辑该/etc/hosts文件(在 Windows 上该文件位于C:\\Windows\\System32\\drivers\\etc)并将主机名分配给127.0.0.1。例如:

\n
127.0.0.1 example.test\n
Run Code Online (Sandbox Code Playgroud)\n

请勿将方案或端口添加到域中,也不应使用常见的扩展名,例如.com.net等,否则可能会与访问互联网上的其他网站发生冲突。

\n

访问下面的域 A 后,您需要单击submit按钮来执行POST/submit路由的请求以开始重定向。请求的唯一原因POST是因为您在示例中使用它,并且我假设您必须发布一些form-data. GET否则,您也可以使用请求。在应用程序 B 中,当执行RedirectResponse从路由POST(即/submit)到GET路由(即/)时,响应状态代码更改为status.HTTP_303_SEE_OTHER,如此处此处此处所述。应用程序 A 正在侦听端口8000,而应用程序 B 正在侦听端口8001

\n

运行下面两个应用程序,然后访问域 A http://127.0.0.1:8000/

\n

应用程序A.py

\n
127.0.0.1 example.test\n
Run Code Online (Sandbox Code Playgroud)\n

应用程序B.py

\n
from fastapi import FastAPI\nfrom fastapi.responses import RedirectResponse, HTMLResponse\nimport uvicorn\n\napp = FastAPI()\n           \n@app.get(\'/\', response_class=HTMLResponse)\ndef home():\n    return """\n    <!DOCTYPE html>\n    <html>\n       <body>\n          <h2>Click the "submit" button to be redirected to domain B</h2>\n          <form method="POST" action="/submit">\n             <input type="submit" value="Submit">\n          </form>\n       </body>\n    </html>\n    """\n        \n@app.post("/submit")\ndef submit():\n    token = \'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\'\n    redirect_url = f\'http://example.test:8001/submit?token={token}\'\n    response = RedirectResponse(redirect_url)\n    response.set_cookie(key=\'access-token\', value=token, httponly=True)  # set cookie for domain A too\n    return response\n \nif __name__ == \'__main__\':\n    uvicorn.run(app, host=\'0.0.0.0\', port=8000)\n
Run Code Online (Sandbox Code Playgroud)\n

解决方案2

\n

另一种解决方案是使用Window.postMessage(),它可以实现对象cross-origin之间的通信;Window例如,页面和pop-up它生成的页面之间,或者页面和iframe嵌入其中的页面之间。有关如何添加事件侦听器以及如何在窗口之间进行通信的示例可以在此处找到。遵循的步骤是:

\n

步骤1:在域A中添加一个隐藏iframe域B。例如:

\n
<iframe id="cross_domain_page" src="http://example.test:8001" frameborder="0" scrolling="no" style="background:transparent;margin:auto;display:block"></iframe>\n
Run Code Online (Sandbox Code Playgroud)\n

步骤2:从A域的异步JS请求Authorization头中获取到token后,立即将其发送到B域。例如:

\n
document.getElementById(\'cross_domain_page\').contentWindow.postMessage(token,"http://example.test:8001");\n
Run Code Online (Sandbox Code Playgroud)\n

步骤3:在域B中,通过 接收token window.addEventListener("message", (event) ...,并将其存储在 中localStorage

\n
localStorage.setItem(\'token\', event.data);\n
Run Code Online (Sandbox Code Playgroud)\n

或者,在使用 JS 的 cookie 中(不推荐,请参阅下面的注释):

\n
document.cookie = `token=${event.data}; path=/; SameSite=None; Secure`;\n
Run Code Online (Sandbox Code Playgroud)\n

步骤 4:向域 A 发送令牌已存储的消息,然后将用户重定向到域 B。

\n
\n

注1:第3步演示了如何使用JS设置cookie,但是当你要存储此类敏感信息时,你不应该真正使用JS,因为通过JS创建的cookie不能包含flag HttpOnly这有助于减轻交叉攻击- 站点脚本(XSS)攻击。这意味着可能向您的网站注入恶意脚本的攻击者将能够访问 cookie。您应该让服务器设置 cookie(通过fetch请求),包括HttpOnly标志(如下例所示),从而使 JS Document.cookieAPI 无法访问 cookie。它还localStorage容易受到 XSS 攻击,因为数据也可以通过 JS 访问(例如localStorage.getItem(\'token\'))。

\n

注 2:要使此解决方案发挥作用,用户必须Allow all cookies在其浏览器\xe2\x80\x94 中启用许多用户没有启用的选项,并且某些浏览器默认排除第三方cookie(Safari 和在私人模式下) Chrome 默认情况下会拒绝这些 cookie)\xe2\x80\x94,因为内容正在iframe从不同的域加载到 中,因此该 cookie 被归类为第三方cookie。这同样适用于使用localStorage(即Allow all cookies必须启用才能通过iframe)。但是,在这种情况下使用 cookie,您还需要将SameSite标志设置为None,并且 cookie 应该包含该Secure标志,这是使用 所必需的SameSite=None。这意味着 cookie 将仅通过HTTPS连接发送;这不会减轻与跨站点访问相关的所有风险,但它将提供针对网络攻击的保护(如果您的服务器不通过 HTTPS 运行,仅出于演示目的,您可以在 Chrome 浏览器中使用\'Insecure origins treated as secure\'实验性功能chrome://flags/)。设置SameSite=None意味着cookie不会受到外部访问的保护,因此您在使用它之前应该意识到风险。

\n

使用iframeSameSite=None; Secure; HttpOnlycookie 的示例

\n

运行下面两个应用程序,然后访问域 A http://127.0.0.1:8000/

\n

应用程序A.py

\n
from fastapi import FastAPI, Request, status\nfrom fastapi.responses import RedirectResponse\nimport uvicorn\n\napp = FastAPI()\n\n@app.get(\'/\')\ndef home(request: Request):\n    token = request.cookies.get(\'access-token\')\n    print(token)\n    return \'You have been successfully redirected to domain B!\' \\\n           f\' Your access token ends with: {token[-4:]}\'\n \n@app.post(\'/submit\')\ndef submit(request: Request, token: str):\n    redirect_url = request.url_for(\'home\')\n    response = RedirectResponse(redirect_url, status_code=status.HTTP_303_SEE_OTHER)\n    response.set_cookie(key=\'access-token\', value=token, httponly=True)\n    return response\n \nif __name__ == \'__main__\':\n    uvicorn.run(app, host=\'0.0.0.0\', port=8001)\n
Run Code Online (Sandbox Code Playgroud)\n

应用程序B.py

\n
<iframe id="cross_domain_page" src="http://example.test:8001" frameborder="0" scrolling="no" style="background:transparent;margin:auto;display:block"></iframe>\n
Run Code Online (Sandbox Code Playgroud)\n

使用iframe和的示例localStorage

\n

此示例演示了一种使用localStorage该时间来存储令牌的方法。一旦存储了令牌,域A就将用户重定向到/redirect域B的路由;然后,域 B 从 中检索令牌localStorage(随后将其从 中删除localStorage),然后将其发送到自己的/submit路由,以便为 设置httpOnlycookie access-token。最后,用户被重定向到域B的主页。

\n

应用程序A.py

\n
document.getElementById(\'cross_domain_page\').contentWindow.postMessage(token,"http://example.test:8001");\n
Run Code Online (Sandbox Code Playgroud)\n

应用程序B.py

\n
from fastapi import FastAPI, Request, Response\nfrom fastapi.responses import HTMLResponse\n\napp = FastAPI()\n\n@app.get(\'/iframe\', response_class=HTMLResponse)\ndef iframe():\n    return """\n    <!DOCTYPE html>\n    <html>\n       <head>\n          <script>\n             window.addEventListener("message", (event) => {\n                if (event.origin !== "http://127.0.0.1:8000")\n                   return;\n             \n                localStorage.setItem(\'token\', event.data);\n                event.source.postMessage("token stored", event.origin);\n             }, false);\n          </script>\n       </head>\n    </html>\n    """\n \n@app.get(\'/redirect\', response_class=HTMLResponse)\ndef redirect():\n    return """\n    <!DOCTYPE html>\n    <html>\n       <head>\n          <script>\n            const token = localStorage.getItem(\'token\');\n            localStorage.removeItem("token");   \n            fetch(\'/submit\', {\n                  method: \'POST\',\n                  headers: {\n                     \'Authorization\': `Bearer ${token}`\n                  }\n               })\n               .then(res => res.text())\n               .then(data => {\n                  window.location.href = \'http://example.test:8001/\';\n               })\n               .catch(error => {\n                  console.error(error);\n               })\n          </script>\n       </head>\n    </html>\n    """\n    \n    \n@app.get(\'/\')\ndef home(request: Request):\n    token = request.cookies.get(\'access-token\')\n    print(token)\n    return \'You have been successfully redirected to domain B!\' \\\n           f\' Your access token ends with: {token[-4:]}\'\n\n@app.post(\'/submit\')\ndef submit(request: Request):\n    authHeader = request.headers.get(\'Authorization\')\n    if authHeader.startswith("Bearer "):\n        token = authHeader[7:]\n    response = Response(\'su


归档时间:

查看次数:

8893 次

最近记录:

1 年,11 月 前