feat(auth): 支持多渠道登录态管理并新增webview重定向功能
- 修改get_current_user函数同时支持Authorization header和cookie获取token - 在logout接口中添加JSONResponse并清除webview cookie登录态 - 新增webview模块实现URL token到cookie的转换和安全重定向功能 - 实现域名白名单校验防止开放重定向漏洞 - 在main.py中注册webview路由
This commit is contained in:
4
main.py
4
main.py
@@ -5,7 +5,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
from server_prd.routes import auth, checkin, course_vote, feedback, pages, plan, profile, survey, yijing
|
from server_prd.routes import auth, checkin, course_vote, feedback, pages, plan, profile, survey, webview, yijing
|
||||||
|
|
||||||
app = FastAPI(title="明缘进阶场 API")
|
app = FastAPI(title="明缘进阶场 API")
|
||||||
app.include_router(auth.router)
|
app.include_router(auth.router)
|
||||||
@@ -17,6 +17,7 @@ app.include_router(survey.router)
|
|||||||
app.include_router(feedback.router)
|
app.include_router(feedback.router)
|
||||||
app.include_router(course_vote.router)
|
app.include_router(course_vote.router)
|
||||||
app.include_router(yijing.router)
|
app.include_router(yijing.router)
|
||||||
|
app.include_router(webview.router)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/v1/health")
|
@app.get("/api/v1/health")
|
||||||
@@ -27,3 +28,4 @@ def health():
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run("server_prd.main:app", host="127.0.0.1", port=8000, reload=True)
|
uvicorn.run("server_prd.main:app", host="127.0.0.1", port=8000, reload=True)
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,20 @@ class BindPhoneRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
def get_current_user(request: Request):
|
def get_current_user(request: Request):
|
||||||
|
token = ""
|
||||||
|
|
||||||
|
# 1. 优先从 Authorization header 取(小程序 API 调用方式)
|
||||||
auth = request.headers.get("Authorization", "")
|
auth = request.headers.get("Authorization", "")
|
||||||
if not auth.startswith("Bearer "):
|
if auth.startswith("Bearer "):
|
||||||
|
token = auth[7:]
|
||||||
|
|
||||||
|
# 2. header 没有则从 cookie 取(webview H5 页面内请求方式)
|
||||||
|
if not token:
|
||||||
|
token = request.cookies.get("auth_token", "")
|
||||||
|
|
||||||
|
if not token:
|
||||||
raise HTTPException(status_code=401, detail="缺少 token")
|
raise HTTPException(status_code=401, detail="缺少 token")
|
||||||
token = auth[7:]
|
|
||||||
session = db.fetch_one(
|
session = db.fetch_one(
|
||||||
"SELECT s.user_id AS id FROM auth_sessions s "
|
"SELECT s.user_id AS id FROM auth_sessions s "
|
||||||
"WHERE s.token=%s AND s.expires_at>NOW()",
|
"WHERE s.token=%s AND s.expires_at>NOW()",
|
||||||
@@ -98,6 +108,8 @@ def verify(user=Depends(get_current_user)):
|
|||||||
|
|
||||||
@router.post("/logout")
|
@router.post("/logout")
|
||||||
def logout(request: Request, user=Depends(get_current_user)):
|
def logout(request: Request, user=Depends(get_current_user)):
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
auth = request.headers.get("Authorization", "")
|
auth = request.headers.get("Authorization", "")
|
||||||
if auth.startswith("Bearer "):
|
if auth.startswith("Bearer "):
|
||||||
db.execute("DELETE FROM auth_sessions WHERE token=%s", (auth[7:],))
|
db.execute("DELETE FROM auth_sessions WHERE token=%s", (auth[7:],))
|
||||||
@@ -106,4 +118,8 @@ def logout(request: Request, user=Depends(get_current_user)):
|
|||||||
"UPDATE auth_users SET wechat_openid=NULL WHERE id=%s",
|
"UPDATE auth_users SET wechat_openid=NULL WHERE id=%s",
|
||||||
(user["id"],),
|
(user["id"],),
|
||||||
)
|
)
|
||||||
return {"success": True}
|
resp = JSONResponse(content={"success": True})
|
||||||
|
# 同时清除 webview cookie 登录态
|
||||||
|
resp.delete_cookie(key="auth_token", path="/")
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|||||||
80
routes/webview.py
Normal file
80
routes/webview.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
"""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
|
||||||
Reference in New Issue
Block a user