如何用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
相关产品推荐
相关产品推荐

