如何在FastAPI中从重定向URL获取#后的参数?
我在使用FastApi处理重定向网址和从查询参数获取数据时遇到了问题。我正在使用Azure AD的授权流程来登录,下面是生成RedirectResponse
的代码。
@app.get("/auth/oauth/{provider_id}")
async def oauth_login(provider_id: str, request: Request):
if config.code.oauth_callback is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No oauth_callback defined",
)
provider = get_oauth_provider(provider_id)
if not provider:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Provider {provider_id} not found",
)
random = random_secret(32)
params = urllib.parse.urlencode(
{
"client_id": provider.client_id,
"redirect_uri": f"{get_user_facing_url(request.url)}/callback",
"state": random,
**provider.authorize_params,
}
)
response = RedirectResponse(
url=f"{provider.authorize_url}?{params}")
samesite = os.environ.get("CHAINLIT_COOKIE_SAMESITE", "lax") # type: Any
secure = samesite.lower() == "none"
response.set_cookie(
"oauth_state",
random,
httponly=True,
samesite=samesite,
secure=secure,
max_age=3 * 60,
)
return response
这是我收到重定向网址的地方。
@app.get("/auth/oauth/{provider_id}/callback")
async def oauth_callback(
provider_id: str,
request: Request,
error: Optional[str] = None,
code: Optional[str] = None,
state: Optional[str] = None,
):
if config.code.oauth_callback is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No oauth_callback defined",
)
provider = get_oauth_provider(provider_id)
if not provider:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Provider {provider_id} not found",
)
if not code or not state:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Missing code or state",
)
response.delete_cookie("oauth_state")
return response
当查询参数以问号(?)开头时,这个重定向工作得很好。但现在的问题是,Azure AD的重定向回调是以井号(#)开头的,因此我无法从网址中获取Code
和State
的查询参数。
带有#
的重定向网址示例
http://localhost/callback#code=xxxxxx&state=yyyyyy
有没有什么想法可以解决这个问题。
1 个回答
在服务器端获取哈希标记 #
后面的文本(或者说键值对)是不可能的。这是因为哈希部分从来不会被发送到服务器(相关的帖子可以在 这里 和 这里 找到)。
我建议你使用URL中的问号 ?
,这是在HTTP请求中发送查询参数的正确方式。如果这是一个复杂的路径参数,你可以参考 这个回答,它可以让你捕获整个URL路径,包括像 /
和 %
这样的字符,但仍然无法获取 #
后面的文本或值。
更新
因为“片段”只在客户端可用/可访问,你可以使用JavaScript来获取 Location
接口的 hash
属性,也就是 window.location.hash
。为此,你可以设置一个 /callback_init
的端点,最初调用它并用作授权服务器的 redirect_uri
,然后返回相关的JavaScript代码来读取片段,并将其作为查询字符串传递到最终的 /callback
端点。这可以通过将URL中的 #
替换为 ?
来轻松实现,如下所示:
@app.get('/callback_init', response_class=HTMLResponse)
async def callback_init(request: Request):
html_content = """
<html>
<head>
<script>
var url = window.location.href;
newUrl = url.replace('/callback_init', '/callback').replace("#", "?");
window.location.replace(newUrl);
</script>
</head>
</html>
"""
return HTMLResponse(content=html_content, status_code=200)
不过,上述方法不会考虑URL中已经存在的查询参数(尽管在你的情况下这不是问题);因此,可以使用下面的方法。
下面的示例还考虑到你设置了一个cookie,之后需要将其删除;因此,这里也进行了演示。此外,请注意,为了在浏览器的地址栏中替换URL(换句话说,发送请求到 /callback
端点),使用了 window.location.replace()
,正如在 这个回答 中解释的那样,这不会让当前页面(在导航到下一个页面之前)保存在会话历史中,这意味着用户无法使用浏览器的后退按钮返回到它(如果出于某种原因你需要允许用户返回,可以使用 window.location.href
或 window.location.assign()
)。
如果你想隐藏URL中的路径和/或查询参数,可以使用类似于 这个回答 和 这个回答 的方法。不过,这并不意味着包含这些路径/查询参数的URL不会进入浏览历史等。因此,你应该注意,在查询字符串中发送敏感信息是不安全的——请参考 这个回答 以获取更多相关信息。
工作示例
要触发重定向,请在浏览器中访问 http://localhost:8000/
。
from fastapi import FastAPI, Request, Response
from fastapi.responses import RedirectResponse, HTMLResponse
from typing import Optional
app = FastAPI()
@app.get("/")
async def main():
redirect_url = 'http://localhost:8000/callback_init?msg=Hello#code=1111&state=2222'
response = RedirectResponse(redirect_url)
response.set_cookie(key='some-cookie', value='some-cookie-value', httponly=True)
return response
@app.get('/callback_init', response_class=HTMLResponse)
async def callback_init(request: Request):
html_content = """
<html>
<head>
<script>
var url = window.location.href;
const fragment = window.location.hash;
const searchParams = new URLSearchParams(fragment.substring(1));
url = url.replace('/callback_init', '/callback').replace(fragment, "");
const newUrl = new URL(url);
for (const [key, value] of searchParams) {
newUrl.searchParams.append(key, value);
}
window.location.replace(newUrl);
</script>
</head>
</html>
"""
return HTMLResponse(content=html_content, status_code=200)
@app.get("/callback")
async def callback(
request: Request,
response: Response,
code: Optional[str] = None,
state: Optional[str] = None,
):
print(request.url.query)
print(request.cookies)
response.delete_cookie("some-cookie")
return {"code": code, "state": state}