"""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