- 修改get_current_user函数同时支持Authorization header和cookie获取token - 在logout接口中添加JSONResponse并清除webview cookie登录态 - 新增webview模块实现URL token到cookie的转换和安全重定向功能 - 实现域名白名单校验防止开放重定向漏洞 - 在main.py中注册webview路由
81 lines
2.8 KiB
Python
81 lines
2.8 KiB
Python
"""Webview 登录态传递:URL token → cookie → 302 重定向。
|
||
|
||
流程:
|
||
1. 小程序端构造 webview URL:/api/v1/webview/redirect?token=xxx&target=xxx
|
||
2. web-view 打开该地址,后端验证 token 有效 → 设置 HttpOnly cookie → 302 重定向到 target
|
||
3. H5 页面内所有同域请求自动携带 cookie,get_current_user 同时支持 header 和 cookie
|
||
|
||
H5 端零改动,权限校验由 H5 自己的接口完成。
|
||
"""
|
||
import logging
|
||
from datetime import datetime
|
||
from urllib.parse import urlparse
|
||
|
||
from fastapi import APIRouter, HTTPException, Query, Request
|
||
from fastapi.responses import RedirectResponse
|
||
|
||
from .. import config, db
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter(prefix="/api/v1/webview", tags=["webview"])
|
||
|
||
# 允许跳转的目标域名白名单(防止开放重定向漏洞)
|
||
ALLOWED_REDIRECT_HOSTS = {"m.muguaclub.cn", "muguaclub.cn"}
|
||
|
||
|
||
def _is_safe_target(target: str) -> bool:
|
||
"""校验 target 是否在允许的域名范围内,防止开放重定向漏洞。"""
|
||
if not target:
|
||
return False
|
||
# 相对路径(/xxx 开头)一律允许,同站跳转
|
||
if target.startswith("/") and not target.startswith("//"):
|
||
return True
|
||
parsed = urlparse(target)
|
||
if not parsed.scheme or not parsed.netloc:
|
||
return False
|
||
if parsed.scheme not in ("http", "https"):
|
||
return False
|
||
host = parsed.netloc.lower().split(":")[0]
|
||
return host in ALLOWED_REDIRECT_HOSTS or any(
|
||
host.endswith("." + h) for h in ALLOWED_REDIRECT_HOSTS
|
||
)
|
||
|
||
|
||
@router.get("/redirect")
|
||
def redirect_with_cookie(
|
||
request: Request,
|
||
token: str = Query(..., description="会话 token"),
|
||
target: str = Query(..., description="目标跳转地址"),
|
||
):
|
||
"""验证 token,设置登录态 cookie,然后 302 重定向到 target。
|
||
|
||
这个接口由 web-view 直接打开,不需要 Authorization header,
|
||
token 通过 URL query 传入,换 cookie 后重定向,URL 中不再暴露 token。
|
||
"""
|
||
if not token or len(token) > 128:
|
||
raise HTTPException(status_code=400, detail="无效的 token")
|
||
if not _is_safe_target(target):
|
||
raise HTTPException(status_code=400, detail="无效的跳转地址")
|
||
|
||
# 验证 token 有效
|
||
session = db.fetch_one(
|
||
"SELECT user_id FROM auth_sessions WHERE token=%s AND expires_at>NOW()",
|
||
(token,),
|
||
)
|
||
if not session:
|
||
raise HTTPException(status_code=401, detail="token 无效或过期")
|
||
|
||
# 设置 HttpOnly cookie + 302 重定向
|
||
response = RedirectResponse(url=target, status_code=302)
|
||
response.set_cookie(
|
||
key="auth_token",
|
||
value=token,
|
||
max_age=int(config.JWT_TTL_SECONDS),
|
||
httponly=True,
|
||
secure=request.url.scheme == "https",
|
||
samesite="lax",
|
||
path="/",
|
||
)
|
||
return response
|