如何从一个域重定向到另一个域并为另一个域设置 cookie 或标头?
- 2025-01-06 08:32:00
- admin 原创
- 118
问题描述:
我正在使用 FastAPI RedirectResponse
,并尝试将用户从一个应用程序(域)重定向到另一个应用程序(域),并在其中设置一些 cookie Response
;但是,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:
如此处所述,无论您使用哪种语言或框架,都无法使用自定义标头集重定向到另一个域。HTTP 协议中的重定向基本上是Location
与响应关联的标头(即),并且不允许添加任何指向目标位置的标头。当您Authorization
在示例中添加标头时,您基本上是为指示浏览器重定向的响应设置该标头,而不是为重定向本身设置该标头。换句话说,您正在将该标头发送回客户端。
至于HTTP cookies,浏览器会将服务器发送的 cookies 与响应一起存储(使用Set-Cookie
标头),然后将这些 cookies 与对同一服务器的请求一起发送到 HTTP 标头中Cookie
。根据文档:
HTTP
Set-Cookie
响应标头用于将 Cookie 从服务器发送到用户代理,以便用户代理稍后将其发送回服务器。要发送多个 Cookie,Set-Cookie
应在同一响应中发送多个标头。
因此,如果这是从一个应用程序(具有子域,例如abc.example.test
)到另一个应用程序(具有子域,例如xyz.example.test
)的重定向,并且两个应用程序都具有相同的(父)域(并且在创建 cookie 时domain
将标志设置为example.test
),则 cookie 将在两个应用程序之间成功共享(好像指定domain
了,那么子域始终包含在内)。 无论使用哪种协议(HTTP/HTTPS)或端口,浏览器都会向给定的域(包括任何子域)提供 cookie。 您可以使用domain
和path
标志限制 cookie 的可用性,也可以使用和标志限制对 cookie 的访问(请参阅此处和此处,以及Starlette 文档)。 如果未设置标志,潜在的攻击者可以通过 JavaScript(JS)读取和修改信息,而具有属性的 cookie 只会发送到服务器,并且客户端的 JS 无法访问。secure
`httpOnlyhttpOnly
httpOnly`
但是,您不能为不同的域设置 cookie。如果允许这样做,将带来巨大的安全漏洞。因此,由于您“试图将用户从一个应用程序(域)重定向到另一个设置了某些 cookie 的应用程序……”,因此它不会起作用,因为 cookie 只会随对同一域的请求一起发送。
解决方案 1
如此处所述,一种解决方案是让域(应用程序)A 将用户重定向到域(应用程序)B,并将access-token
URL 作为查询参数传入。然后,域 B 将读取令牌并设置自己的 cookie,以便浏览器将存储该 cookie 并在每次对域 B 的后续请求中发送该 cookie。
请注意,您应考虑使用安全 (HTTPS) 通信,以便令牌以加密方式传输,并secure
在创建 cookie 时设置标志。另请注意,在查询字符串中包含令牌 会带来严重的安全风险,因为敏感数据绝不能通过查询字符串传递。这是因为查询字符串(URL 的一部分)出现在浏览器的地址栏中;因此,允许用户查看和收藏包含令牌的 URL(意味着它保存在磁盘上)。此外,URL 会进入浏览历史记录,这意味着它无论如何都会写入磁盘并出现在History
选项卡中(按下Ctrl+H
可查看浏览器的历史记录)。以上两种情况都会允许攻击者(以及与您共享计算机/移动设备的人)窃取此类敏感数据。此外,许多浏览器插件/扩展会跟踪用户的浏览活动 - 您访问的每个 URL 都会发送到他们的服务器进行分析,以便检测恶意网站并提前警告您。因此,在使用以下方法之前,您应该考虑以上所有因素(有关此主题的相关帖子,请参见此处、此处和此处)。
为了防止在地址栏中显示 URL,下面的方法也使用了域 B 内的重定向。一旦域 B 收到/submit
以令牌作为查询参数的路由请求,域 B 就会重定向到其中没有令牌的裸 URL(即其home
页面)。由于这种重定向,其中包含令牌的 URL 不会出现在浏览历史记录中。虽然这可以在一定程度上防止前面描述的某些攻击,但这并不意味着浏览器扩展等将无法捕获其中包含令牌的 URL。
如果您在 localhost 上测试此操作,则需要为应用程序 B 指定一个不同的域名;否则,如前所述,cookie 将在具有相同域的应用程序之间共享,因此,您最终将收到为域 A 设置的 cookie,并且无法判断该方法是否有效。为此,您必须编辑文件/etc/hosts
(在 Windows 上,它位于C:WindowsSystem32driversetc
)并为 分配主机名127.0.0.1
。例如:
127.0.0.1 example.test
请勿将方案或端口添加到域名,也不要使用常见扩展名,例如.com
、.net
等,否则可能会与访问互联网上的其他网站发生冲突。
在下面访问域 A 后,您需要单击按钮以向路由submit
执行请求以开始重定向。请求的唯一原因是因为您在示例中使用它,并且我假设您必须发布一些。否则,您也可以使用请求。在应用程序 B 中,当执行从路由(即)到路由(即)的 时,响应状态代码将更改为,如此处、此处和此处所述。应用程序 A 正在监听端口,而应用程序 B 正在监听端口。POST
`/submitPOST
form-dataGET
RedirectResponsePOST
/submitGET
/status.HTTP_303_SEE_OTHER
8000`8001
运行以下两个应用程序,然后通过http://127.0.0.1:8000/访问域A。
应用程序A.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)
应用程序B.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!' \n 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()
,它允许对象cross-origin
之间进行通信Window
;例如,页面与其pop-up
生成的 之间,或页面与其iframe
嵌入的 之间。有关如何添加事件侦听器和窗口之间通信的示例可在此处找到。要遵循的步骤如下:
步骤 1:将域 A 添加到域 B 的隐藏iframe
。例如:
<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
的headers中获取token后,立即将其发送给域B。例如:
document.getElementById('cross_domain_page').contentWindow.postMessage(token,"http://example.test:8001");
步骤3:在域B中,通过接收令牌window.addEventListener("message", (event) ...
,并将其存储在localStorage
:
localStorage.setItem('token', event.data);
或者,在 cookie 中使用 JS(不推荐,请参阅下面的注释):
document.cookie = `token=${event.data}; path=/; SameSite=None; Secure`;
步骤4:向域A发送消息,告知其令牌已存储,然后将用户重定向到域B。
注意 1:步骤 3 演示了如何使用 JS 设置 cookie,但当您要存储此类敏感信息时,您实际上不应该使用JS,因为通过 JS 创建的 cookie 不能包含HttpOnly
标志,这有助于缓解跨站点脚本 ( XSS ) 攻击。这意味着可能已将恶意脚本注入您网站的攻击者将能够访问该 cookie。您应该让服务器设置 cookie(通过fetch
请求),包括HttpOnly
标志(如下例所示),从而使 JS Document.cookie
API 无法访问该 cookie。localStorage
也容易受到 XSS 攻击,因为数据也可以通过 JS 访问(例如localStorage.getItem('token')
)。
注 2:要使此解决方案起作用,用户必须Allow all cookies
在其浏览器中启用该选项(许多用户没有这样做,并且某些浏览器默认排除第三方cookie(众所周知,Safari 和 Chrome 的私人模式默认拒绝这些 cookie))- 因为内容是iframe
从不同的域加载到的,因此 cookie 被归类为第三方cookie。这同样适用于使用localStorage
(即Allow all cookies
必须启用才能通过使用它iframe
)。但是,在这种情况下使用 cookie,您还需要将SameSite
标志设置为None
,并且 cookie 应包含Secure
标志,这是使用所必需的。这意味着 cookie 将仅通过SameSite=None
HTTPS连接发送;这不会减轻与跨站点访问相关的所有风险,但它将提供针对网络攻击的保护(如果您的服务器不通过 HTTPS 运行,仅出于演示目的,您可以在 Chrome 浏览器中使用'Insecure origins treated as secure'
实验性功能)。设置意味着 cookie将不受保护以防止外部访问,因此您在使用它之前应该意识到风险。chrome://flags/
`SameSite=None`
使用cookieiframe
的示例SameSite=None; Secure; HttpOnly
运行以下两个应用程序,然后通过http://127.0.0.1:8000/访问域A。
应用程序A.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)
应用程序B.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!' \n 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)
iframe
使用和的示例localStorage
此示例演示了使用此时间来存储令牌的方法localStorage
。令牌存储后,域 A 将用户重定向到/redirect
域 B 的路由;域 B 然后从 检索令牌localStorage
(随后将其从 中删除localStorage
),然后将其发送到自己的/submit
路由,以便为 设置httpOnly
cookie access-token
。最后,用户被重定向到域 B 的主页。
应用程序A.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)
应用程序B.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!' \n 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,而是localStorage
与其集中式域http://stackauth.com上的全局身份验证相结合。即使不使用第三方 cookie,他们也会使用(如此处iframe
所述)将令牌存储在中。这意味着,这种方法只有在用户的浏览器接受第三方cookie 时才会起作用(如解决方案 2 中所述)。如解决方案 2 中所述,即使您正在访问而不是在主窗口中嵌入,您仍然需要用户在其浏览器设置中启用它;否则,它将不起作用,如果用户尝试访问SE 网络中的任何其他站点,它将被要求再次登录。localStorage
`localStoragedocument.cookie
iframe`Allow all cookies
更新
上面描述的方法是 SE 自动登录过去的工作方式。如今,方法略有不同,如最近的一篇文章中所述,该文章实际上描述了 SE 通用登录现在的工作方式(您可以在登录 SE 站点之一时,通过检查浏览器 DevTools 中的网络活动来验证该过程;例如,SO)。
其工作方式是,<img>
当您登录 SE 站点之一时,注入指向其他 Stack Exchange 站点(即 serverfault.com、superuser.com 等)的标签。src
这些 <img>
标签的 URL 包含一个唯一的authToken
查询参数,该参数由通用身份验证系统生成并通过请求获取XMLHttpRequest
POST
。这些标签的示例<img>
如下:
<img src="https://serverfault.com/users/login/universal.gif?authToken=<some-token-value>&nonce=<some-nonce-value>" />
src
然后,您的浏览器会将该URL(其中包含)发送authToken
到其他每个网站(您当前不在的),并且作为对该图像的响应,每个给定的域/网站将返回两个 cookie:prov
和acct
。当您稍后切换到其他 SE 网站之一时,您的浏览器将发送您之前收到的 cookieprov
和acct
,以便该网站验证令牌并(如果有效)让您登录。
注意:要使其正常工作,您的浏览器需要接受第三方cookie(如前所述),因为必须使用SameSite=None; Secure
标志设置 cookie(请注意上述风险)。如果不允许第三方 cookie(以及不通过 HTTPS 运行服务器),通用自动登录将无法工作。此外,您尝试为其设置 cookie 的其他域需要启用CORSimg
,因为当从其他域加载时,会执行跨域authToken
请求。此外,由于这种方法会在 URL 的查询参数中发送(即使它发生在后台而不是浏览器的地址栏中),您应该注意解决方案 1 中前面描述的风险。
下面使用<img>
指向域 B 的标签。 img URL 不必是服务器接收的实际图像access-token
,因此,您可以使用该.onerror()
函数来检查请求何时实际完成(意味着服务器已使用Set-Cookie
标头做出响应),以便您可以将用户重定向到域 B。
也可以使用在标头中fetch
包含 的对域 B 的请求,服务器可以做出类似响应以设置 cookie。在这种情况下,请确保使用和,并在服务器端明确指定允许的来源,如此处所述。access-token
`Authorizationcredentials: 'include'
mode: 'cors'`
运行以下两个应用程序,然后通过http://127.0.0.1:8000/访问域A。
应用程序A.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)
应用程序B.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!' \n 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)
扫码咨询,免费领取项目管理大礼包!