You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Vanilla JS为FastAPI+Jinja2 SSR添加请求授权头并实现导航

问题描述

我用原生JS的fetch给请求加了Authorization Header,但FastAPI返回的HTMLResponse被当成文本而非导航用的文档。fetch请求的Sec-fetch-dest头是"empty",本该是"document",手动修改后浏览器又自动改回"empty"。点击<a href="/private">我在公开页面,点击进入私有页面</a>链接,接口返回text/html且无报错,但页面完全没变化。想问下不用浏览器扩展拦截器的话,还有啥用原生JS加Authorization Header的方法?


解决方案

1. 直接替换页面内容(最简单方案)

fetch拿到HTML响应后,直接替换当前页面的全部内容,同时更新浏览器历史记录,模拟正常导航效果。修改public.html里的脚本:

<script>
const element = document.querySelector("a");
element.addEventListener('click', async (event) => {
    event.preventDefault();
    const url = event.target.getAttribute('href');
    const options = {
        method: "GET",
        headers: {
            "Authorization": "bearer gvv"
        }
    };
    const response = await fetch(url, options);
    if (response.ok) {
        const html = await response.text();
        // 替换整个页面的HTML
        document.documentElement.innerHTML = html;
        // 更新浏览器地址栏和历史记录,让后退按钮正常工作
        history.pushState({}, '', url);
    }
});
</script>

2. 改用Cookie存储Token(最符合浏览器行为的方案)

浏览器原生导航(比如点击a标签)不会带上自定义Authorization头,但会自动携带Cookie。可以把Token存在Cookie里,让FastAPI从Cookie读取Token验证身份,这样不用JS拦截请求,直接用原生a标签就能正常跳转。

修改FastAPI代码:

from fastapi import FastAPI, Request, Depends, Cookie, Response
from fastapi.templating import Jinja2Templates
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from typing import Dict

templates = Jinja2Templates(directory="")
app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# 新增依赖:优先从Cookie取Token,没有则从Header取
def get_token(token: str = Depends(oauth2_scheme), access_token: str = Cookie(None)):
    return access_token or token

def get_current_user(token: str = Depends(get_token)):
    return dict(id=1, name="george")

# 修改Token接口,把Token写入Cookie
@app.post("/token")
def get_access_token(request: Request,
                     form_data: OAuth2PasswordRequestForm = Depends(),
                     response: Response = None):
    token_data = dict(access_token="gvv", token_type="bearer")
    # 设置HttpOnly Cookie,防止XSS攻击,更安全
    response.set_cookie(key="access_token", value=token_data["access_token"], httponly=True)
    return token_data

@app.get("/")
def public_page(request: Request):
    return templates.TemplateResponse("public.html", {"request": request})

@app.get("/private")
def private_page(request: Request,
                 current_user: Dict = Depends(get_current_user)):
    return templates.TemplateResponse("private.html", {"request": request})

修改public.html:

删掉原来的JS拦截代码,直接保留原生a标签:

{% extends "./base.html" %}
{% block content %}
  <a href="/private">我在公开页面,点击进入私有页面</a>
{% endblock %}

点击链接时浏览器会自动带上Cookie,FastAPI能正常验证身份,页面也会正常跳转,完全符合浏览器原生行为。

3. 用隐藏iframe中转(适合复杂场景)

创建隐藏iframe,用fetch发起带Authorization头的请求,把HTML内容写入iframe后,再提取到当前页面:

<script>
const element = document.querySelector("a");
element.addEventListener('click', (event) => {
    event.preventDefault();
    const url = event.target.getAttribute('href');
    
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    
    fetch(url, {
        headers: { "Authorization": "bearer gvv" }
    }).then(res => res.text()).then(html => {
        iframe.contentDocument.open();
        iframe.contentDocument.write(html);
        iframe.contentDocument.close();
        
        iframe.onload = () => {
            // 替换当前页面内容
            document.documentElement.innerHTML = iframe.contentDocument.documentElement.innerHTML;
            // 更新历史记录
            history.pushState({}, '', url);
            // 移除iframe
            document.body.removeChild(iframe);
        };
    });
    
    document.body.appendChild(iframe);
});
</script>

原代码参考

main.py

from fastapi import FastAPI, Request, Depends
from fastapi.templating import Jinja2Templates
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from typing import Dict

templates = Jinja2Templates(directory="")
app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def get_current_user(token: str = Depends(oauth2_scheme)):
    return dict(id=1, name="george")

@app.post("/token")
def get_access_token(request: Request,
                     form_data: OAuth2PasswordRequestForm = Depends()):
    return dict(access_token="gvv", token_type="bearer")

@app.get("/")
def public_page(request: Request):
    return templates.TemplateResponse("public.html", {"request": request})

@app.get("/private")
def private_page(request: Request,
                 current_user: Dict = Depends(get_current_user)):
    return templates.TemplateResponse("private.html", {"request": request})

base.html

<!DOCTYPE html>
<html lang="en">
   <head>
      <meta charset="UTF-8">
      <title>Test</title>
  </head>
  <body>
     {% block content %}
     {% endblock %}
  </body>
</html>

public.html

{% extends "./base.html" %}
{% block content %}
  <a href="/private">I am in Public. Click to go to Private</a>

  <script>
    const element = document.querySelector("a");
    element.addEventListener('click', (event) => {
        event.preventDefault();
        const url = event.target.getAttribute('href');
        const options = {
            method: "GET",
            headers: {
                "Authorization": "bearer gvv"
            }
        };
        return fetch(url, options);
    });
  </script>
{% endblock %}

private.html

{% extends "./base.html" %}
{% block content %}
   <a href="/">I am in Private. Click to go to Public</a>
{% endblock %}

内容的提问来源于stack exchange,提问作者gvv

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 19:17:09