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

8
我正在使用FastAPI的RedirectResponse,尝试将用户从一个应用程序(域)重定向到另一个应用程序,并在response中设置一些cookie,但是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

我该如何解决这个问题?提前感谢您的帮助。
1个回答

15

正如这里所描述的那样,无论您使用什么语言或框架,都不能设置自定义标头并重定向到另一个域。在HTTP协议中,重定向基本上是与响应相关联的标头(即Location),它不允许添加任何标头到目标位置。当您在示例中添加Authorization标头时,实际上是为响应设置了该标头,指示浏览器进行重定向,而不是针对重定向本身。换句话说,您正在将该标头发送回客户端

关于HTTP cookies,浏览器会使用响应中的Set-Cookie报头存储服务器发送的cookie,并在稍后发送同一服务器的请求时,将这些cookie与Cookie HTTP报头一起发送。根据文档的说明:

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

因此,如果这是从一个应用程序(具有子域,例如 abc.example.test)重定向到另一个应用程序(具有子域,例如 xyz.example.test),两个应用程序都具有相同的(父)域(并且在创建 cookie 时设置了 domain 标志为 example.test),则 cookie 将在两个应用程序之间成功共享(如 如果指定了 domain,则始终包括子域)。浏览器将使 cookie 可用于给定的域,包括任何子域,无论使用哪种协议(HTTP/HTTPS)或端口。您可以使用 domainpath 标志限制 cookie 的可用性,以及使用 securehttpOnly 标志限制访问 cookie(请参见 此处此处,以及 Starlette 文档)。如果未设置 httpOnly 标志,则潜在攻击者可以通过 JavaScript(JS)读取和修改信息,而具有 httpOnly 属性的 cookie 仅发送到服务器,并且对客户端上的 JS 不可访问。
然而,您无法为不同的域设置cookie。如果允许这样做,将会出现巨大的安全漏洞。因此,由于您正在尝试将用户从一个应用程序(域)重定向到另一个应用程序(域)并设置一些cookie,这是行不通的,因为cookie只会随着发送到相同域的请求一起发送。

解决方案1

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

请注意,您应该考虑使用安全的(HTTPS)通信方式,以便令牌被传输时进行加密,并在创建cookie时设置“secure”标志。此外,请注意,在查询字符串中包含令牌会带来严重的安全风险,因为敏感数据不应通过查询字符串传递。这是因为查询字符串是URL的一部分,出现在浏览器的地址栏中;因此,允许用户查看并将带有令牌的URL添加到书签中(这意味着它保存在磁盘上)。而且,URL将出现在浏览历史记录中,这意味着它将被写入磁盘并出现在History选项卡中(按Ctrl+H查看浏览器的历史记录)。以上两者都会允许攻击者(以及与您共享计算机/移动设备的人)窃取此类敏感数据。此外,许多浏览器插件/扩展跟踪用户的浏览活动-您访问的每个URL都会发送到其服务器进行分析,以便检测恶意网站并事先警告您。因此,在使用以下方法之前,您应该考虑所有以上内容(有关此主题的相关帖子,请参见这里这里这里)。
为了防止在地址栏中显示URL,以下方法还在域B内使用重定向。一旦域B接收到带有令牌作为查询参数的/submit路由的请求,域B将响应重定向到一个没有令牌的裸URL(即其home页面)。由于这个重定向,带有令牌的URL不会出现在浏览历史记录中。虽然这提供了一些保护措施来防范前面描述的某些攻击,但这并不意味着浏览器扩展等无法捕获带有令牌的URL。
如果您在本地主机上进行测试,则需要为应用程序B指定不同的域名;否则,如前所述,具有相同域的应用程序之间将共享cookie,因此您最终会收到为域A设置的cookie,并且无法判断该方法是否起作用。要做到这一点,您必须编辑/etc/hosts文件(在Windows上位于C:\Windows\System32\drivers\etc),并将主机名分配给127.0.0.1。例如:
127.0.0.1 example.test

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

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

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

appA.py

from fastapi import FastAPI
from fastapi.responses import RedirectResponse, HTMLResponse
import uvicorn

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

appB.py

from fastapi import FastAPI, Request, status
from fastapi.responses import RedirectResponse
import uvicorn

app = FastAPI()

@app.get('/')
def home(request: Request):
    token = request.cookies.get('access-token')
    print(token)
    return 'You have been successfully redirected to domain B!' \
           f' Your access token ends with: {token[-4:]}'
 
@app.post('/submit')
def submit(request: Request, token: str):
    redirect_url = request.url_for('home')
    response = RedirectResponse(redirect_url, status_code=status.HTTP_303_SEE_OTHER)
    response.set_cookie(key='access-token', value=token, httponly=True)
    return response
 
if __name__ == '__main__':
    uvicorn.run(app, host='0.0.0.0', port=8001)

解决方案2

另一个解决方案是使用Window.postMessage(),它可以在Window对象之间启用跨源通信;例如,在页面和它生成的pop-up之间,或者在嵌入其中的iframe和页面之间。关于如何添加事件监听器和在窗口之间进行通信的示例可以在这里找到。需要遵循以下步骤:

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

<iframe id="cross_domain_page" src="http://example.test:8001" frameborder="0" scrolling="no" style="background:transparent;margin:auto;display:block"></iframe>
步骤2:一旦从向域A发出的异步JS请求的头部获取Authorization令牌,立即将其发送到域B。例如:
document.getElementById('cross_domain_page').contentWindow.postMessage(token,"http://example.test:8001");
第三步:在域B中,通过window.addEventListener("message", (event) ...接收令牌,并将其存储在localStorage中。
localStorage.setItem('token', event.data);

或者,使用JS在cookie中(不推荐,请参见下面的注释):
document.cookie = `token=${event.data}; path=/; SameSite=None; Secure`;

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

注意 1:第三步演示了如何使用JS设置cookie,但是当您要存储此类敏感信息时,不应该真的使用JS,因为通过JS创建的cookie无法包含HttpOnly标志,这有助于减轻跨站点脚本(XSS)攻击。这意味着可能已经将恶意脚本注入到您的网站中的攻击者将能够访问cookie。相反,您应该让服务器设置cookie(通过fetch请求),包括HttpOnly标志(如下例所示),从而使cookie对JS Document.cookie API不可访问。localStorage也容易受到XSS攻击,因为数据也可以通过JS访问(例如,localStorage.getItem('token'))。 注意2:为了使此解决方案起作用,用户必须在其浏览器中启用允许所有Cookie选项——很多用户不会这样做,而一些浏览器默认排除第三方Cookie(Safari和Chrome的In Private模式已知默认拒绝这些Cookie)——因为内容正在从不同的域加载到一个iframe中,因此Cookie被视为 第三方 cookie。对于使用localStorage也是如此(即必须启用允许所有Cookie才能通过iframe使用它)。然而,在这种情况下使用Cookie,您还需要将SameSite标志设置为None,并且Cookie应包括Secure标志,这是必需的,以便使用SameSite=None。这意味着Cookie仅会通过 HTTPS连接发送;这不会消除与跨站点访问相关的所有风险,但它将提供对网络攻击的保护(如果您的服务器未运行在HTTPS上,仅用于演示目的,则可以在Chrome浏览器的chrome://flags/中使用'Insecure origins treated as secure'实验性功能)。设置SameSite=None意味着Cookie将不受保护免受外部访问,因此在使用之前您应该了解风险。

使用 iframeSameSite=None; Secure; HttpOnly cookie 的示例

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

appA.py

from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse

app = FastAPI()

@app.get('/', response_class=HTMLResponse)
def home():
    return """
    <!DOCTYPE html>
    <html>
       <body>
          <h2>Click the "submit" button to be redirected to domain B</h2>
          <input type="button" value="Submit" onclick="submit()"><br>
          <iframe id="cross_domain_page" src="http://example.test:8001/iframe"  frameborder="0" scrolling="no" style="background:transparent;margin:auto;display:block"></iframe>
          <script>
             function submit() {
                fetch('/submit', {
                     method: 'POST',
                  })
                  .then(res => {
                     authHeader = res.headers.get('Authorization');
                     if (authHeader.startsWith("Bearer "))
                        token = authHeader.substring(7, authHeader.length);
                     return res.text();
                  })
                  .then(data => {
                     document.getElementById('cross_domain_page').contentWindow.postMessage(token, "http://example.test:8001");
                  })
                  .catch(error => {
                     console.error(error);
                  });
             }
             
             window.addEventListener("message", (event) => {
                if (event.origin !== "http://example.test:8001")
                  return;
             
                if (event.data == "cookie is set")
                  window.location.href = 'http://example.test:8001/';
             }, false);
          </script>
       </body>
    </html>
    """

@app.post('/submit')
def submit():
    token = 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3'
    headers = {'Authorization': f'Bearer {token}'}
    response = Response('success', headers=headers)
    response.set_cookie(key='access-token', value=token, httponly=True)  # set cookie for domain A too
    return response
    
if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8000)

appB.py

from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse

app = FastAPI()

@app.get('/iframe', response_class=HTMLResponse)
def iframe():
    return """
    <!DOCTYPE html>
    <html>
       <head>
          <script>
             window.addEventListener("message", (event) => {
                if (event.origin !== "http://127.0.0.1:8000")
                   return;
             
                fetch('/submit', {
                      method: 'POST',
                      headers: {
                         'Authorization': `Bearer ${event.data}`
                      }
                   })
                   .then(res => res.text())
                   .then(data => {
                      event.source.postMessage("cookie is set", event.origin);
                   })
                   .catch(error => {
                      console.error(error);
                   })
             }, false);
          </script>
       </head>
    </html>
    """
    
@app.get('/')
def home(request: Request):
    token = request.cookies.get('access-token')
    print(token)
    return 'You have been successfully redirected to domain B!' \
           f' Your access token ends with: {token[-4:]}'

@app.post('/submit')
def submit(request: Request):
    authHeader = request.headers.get('Authorization')
    if authHeader.startswith("Bearer "):
        token = authHeader[7:]
    response = Response('success')
    response.set_cookie(key='access-token', value=token, samesite='none', secure=True, httponly=True) 
    return response
    
if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8001)

使用 iframelocalStorage 的示例

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

appA.py

from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse

app = FastAPI()
    
@app.get('/', response_class=HTMLResponse)
def home():
    return """
    <!DOCTYPE html>
    <html>
       <body>
          <h2>Click the "submit" button to be redirected to domain B</h2>
          <input type="button" value="Submit" onclick="submit()"><br>
          <iframe id="cross_domain_page" src="http://example.test:8001/iframe"  frameborder="0" scrolling="no" style="background:transparent;margin:auto;display:block"></iframe>
          <script>
             function submit() {
                fetch('/submit', {
                     method: 'POST',
                  })
                  .then(res => {
                     authHeader = res.headers.get('Authorization');
                     if (authHeader.startsWith("Bearer "))
                        token = authHeader.substring(7, authHeader.length);
                     return res.text();
                  })
                  .then(data => {
                     document.getElementById('cross_domain_page').contentWindow.postMessage(token, "http://example.test:8001");
                  })
                  .catch(error => {
                     console.error(error);
                  });
             }
             
             window.addEventListener("message", (event) => {
                if (event.origin !== "http://example.test:8001")
                  return;
             
                if (event.data == "token stored")
                  window.location.href = 'http://example.test:8001/redirect';
             }, false);
                
          </script>
       </body>
    </html>
    """

@app.post('/submit')
def submit():
    token = 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3'
    headers = {'Authorization': f'Bearer {token}'}
    response = Response('success', headers=headers)
    response.set_cookie(key='access-token', value=token, httponly=True)  # set cookie for domain A too
    return response
    
if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8000)

appB.py

from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse

app = FastAPI()

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

@app.post('/submit')
def submit(request: Request):
    authHeader = request.headers.get('Authorization')
    if authHeader.startswith("Bearer "):
        token = authHeader[7:]
    response = Response('success')
    response.set_cookie(key='access-token', value=token, httponly=True) 
    return response
  
   
if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8001)

解决方案 3

请看这里这里,了解StackExchange的自动登录是如何工作的;例如,当您已经登录到StackOverflow(SO)时,StackExchange(SE)会自动将您登录。简而言之,他们不使用第三方cookie,而是结合全局认证在他们的集中域http://stackauth.com中使用localStorage。即使不使用第三方cookie,他们也会像这里所述一样使用iframe将令牌存储在localStorage中。这意味着,只有当用户的浏览器接受第三方cookie(如解决方案2中所述),此方法才能起作用。正如解决方案2中所解释的那样,即使您在嵌入主窗口的iframe中访问localStorage而不是document.cookie,您仍然需要用户在其浏览器设置中启用允许所有cookie;否则,它将无法工作,并且如果他们尝试访问SE网络中的任何其他站点,用户将被要求重新登录。

更新

上述方法是 SE 自动登录过去的工作方式。现在,这种方法有些不同,如更近期的帖子所述,实际上描述了 SE 通用登录的工作方式(您可以通过检查浏览器 DevTools 中的网络活动,在登录到其中一个 SE 站点时验证该过程;例如,SO)。

它的工作方式是在您登录到 SE 站点之一时,注入指向其他 Stack Exchange 站点(即 serverfault.com、superuser.com 等)的 <img> 标记。这些 <img> 标记的 src URL 包括一个唯一的 authToken 作为查询参数,该参数是由通用身份验证系统生成并通过 XMLHttpRequest POST 请求获得的。这些 <img> 标记的示例如下:

<img src="https://serverfault.com/users/login/universal.gif?authToken=<some-token-value>&nonce=<some-nonce-value>" />

您的浏览器将发送带有authTokensrc URL到其他站点(您当前不在该站点上),并在响应该图像时,为每个给定的域/站点返回两个cookie:provacct。当您稍后切换到其他SE站点之一时,您的浏览器将发送您先前接收到的provacct cookie,以便站点验证令牌并(如果有效)登录您。

注意: 为使此方法生效,您的浏览器需要接受第三方 cookie(如前所述),因为cookie需要设置SameSite=None; Secure标志(请注意上述风险)。如果不允许第三方cookie——以及不使用HTTPS运行服务器——通用自动登录将无法工作。此外,您正在尝试设置cookie的其他域需要启用CORS,因为当从不同的域加载img时,会执行跨源请求。此外,由于此方法在URL的查询参数中发送authToken(即使它是在后台而不是在浏览器的地址栏中进行的),您应该注意解决方案1中描述的风险。

下面使用指向域 B 的 <img> 标签。图片 URL 不必是实际的图片,服务器仍然可以接收 access-token,因此您可以使用 .onerror() 函数检查请求何时完成(即服务器已响应 Set-Cookie 标头),以便将用户重定向到域 B。
相反,可以使用带有 Authorization 标头的 fetch 请求到域 B,并且服务器可以类似地响应以设置 cookie。在这种情况下,请确保使用 credentials: 'include'mode: 'cors',并明确指定服务器端允许的来源,如此处所述。
在下面同时运行两个应用程序,并随后访问A域名http://127.0.0.1:8000/

appA.py

from fastapi import FastAPI, Response
from fastapi.responses import HTMLResponse

app = FastAPI()

@app.get('/', response_class=HTMLResponse)
def home():
    return """
    <!DOCTYPE html>
    <html>
       <body>
          <h2>Click the "submit" button to be redirected to domain B</h2>
          <input type="button" value="Submit" onclick="submit()"><br>
          <script>
            function submit() {
               fetch('/submit', {
                     method: 'POST',
                  })
                  .then(res => {
                     authHeader = res.headers.get('Authorization');
                     if (authHeader.startsWith("Bearer "))
                        token = authHeader.substring(7, authHeader.length);

                     return res.text();
                  })
                  .then(data => {
                     var url = 'http://example.test:8001/submit?token=' + encodeURIComponent(token);
                     var img = document.createElement('img');
                     img.style = 'display:none';
                     img.crossOrigin = 'use-credentials'; // needed for CORS
                     img.onerror = function(){
                        window.location.href = 'http://example.test:8001/';
                     }
                     img.src = url;
                  })
                  .catch(error => {
                     console.error(error);
                  });
            }
          </script>
       </body>
    </html>
    """
    
@app.post('/submit')
def submit():
    token = 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3'
    headers = {'Authorization': f'Bearer {token}'}
    response = Response('success', headers=headers)
    response.set_cookie(key='access-token', value=token, httponly=True)  # set cookie for domain A too
    return response
    
if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8000)

appB.py

from fastapi import FastAPI, Request, Response
from fastapi.responses import RedirectResponse
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

origins = ['http://localhost:8000', 'http://127.0.0.1:8000',
           'https://localhost:8000', 'https://127.0.0.1:8000'] 

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get('/')
def home(request: Request):
    token = request.cookies.get('access-token')
    print(token)
    return 'You have been successfully redirected to domain B!' \
           f' Your access token ends with: {token[-4:]}'
 
@app.get('/submit')
def submit(request: Request, token: str):
    response = Response('success')
    response.set_cookie(key='access-token', value=token, samesite='none', secure=True, httponly=True) 
    return response

if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8001)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接