如何在FastAPI中从重定向URL获取#后的参数?

1 投票
1 回答
134 浏览
提问于 2025-04-14 17:16

我在使用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的重定向回调是以井号(#)开头的,因此我无法从网址中获取CodeState的查询参数。

带有#的重定向网址示例

http://localhost/callback#code=xxxxxx&state=yyyyyy

有没有什么想法可以解决这个问题。

1 个回答

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.hrefwindow.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}

撰写回答