fastapi: 管理后台:用中间件判断是否登录,有需要登录才能访问的页面直接跳转登录页

一,代码

middleware的代码:

# app/admin/middleware/auth_middleware.py
from urllib.parse import quote_plus
from fastapi import FastAPI, Request, Depends

from sqlalchemy import select
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse, RedirectResponse

from app.core.database import get_db, AsyncSessionLocal
from app.core.jwt import admin_login_optional, get_current_user_from_cookie
from app.models import User

# 白名单列表,管理后台:登录和注册页面
PUBLIC_PATHS = {"/admin/account/login", "/admin/account/register"}

# 中间件:全自动解析登录态
# 1. 定义中间件类,必须继承 BaseHTTPMiddleware
class CustomAuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self,
                       request: Request,
                       call_next,
                       ):
        # 从cookie得到用户信息
        current_user = await get_current_user_from_cookie(request)
        # 如果用户未登录
        if current_user is None:
            #判断url是否必须登录
            # 过滤白名单,无需登录
            if request.url.path in PUBLIC_PATHS:
                # 将结果挂载到 request.state,方便后续调用
                request.state.user = None
                request.state.is_authenticated = False

                response = await call_next(request)
                return response
            else:    # 需要登录
                # 得到当前页地址
                # 1. 获取用户当前访问的相对完整路径+参数 (例如: /profile?template_id=123&mode=edit)
                current_path = request.url.path
                if request.url.query:
                    current_path += f"?{request.url.query}"

                # 2. 🌟 生产核心:对整个路径进行安全 URL 编码
                # /profile?template_id=123  -->  %2Fprofile%3Ftemplate_id%3D123
                encoded_next = quote_plus(current_path)

                # 3. 拼接后跳转。此时 URL 变为:/login?next=%2Fprofile%3Ftemplate_id%3D123
                login_path = "/admin/account/login"
                login_url = f"{login_path}?next={encoded_next}"

                return RedirectResponse(url=login_url, status_code=303)


        # 生产环境核心:直接使用全局工厂创建独立的、短暂的会话,获取用户的完整信息
        try:
            async with AsyncSessionLocal() as db:
                # 1. 查询用户是否存在
                stmt = select(User).where(User.username == current_user['username'])
                result = await db.execute(stmt)
                user = result.scalar_one_or_none()

                if user is None:
                    cur_user = None
                else:
                    cur_user = {
                        'username':user.username,
                        'nickname': user.nickname,
                    }
        except Exception as e:
            # 生产环境中务必捕获数据库异常,防止连接泄漏或暴露敏感堆栈
            return JSONResponse(status_code=500, content={"detail": "Internal server error"})

        # 将结果挂载到 request.state,方便后续调用
        request.state.user = cur_user
        request.state.is_authenticated = cur_user is not None

        response = await call_next(request)
        return response

二,测试效果

image

三,说明:

此处需要说明的一点:

在 FastAPI 的中间件中可以直接主动抛出(raise)异常,但绝对不能直接使用 raise HTTPException()
如果你在中间件中直接 raise HTTPException,FastAPI 的全局异常处理器会无法正确捕捉它,
最终会演变成你上一问遇到的底层 Starlette 崩溃报错,或者直接对客户端返回模糊的 Internal Server Error
解决方法:
直接返回 JSONResponse(最常用、最标准)
在中间件的逻辑中,如果你发现请求不合法(例如黑名单、鉴权失败),最标准且优雅的做法是不使用 raise,而是直接 return 一个响应对象(如 JSONResponse
这样请求会被立即拦截,不会进入后续的路由,也不会触发任何底层错误。
posted @ 2026-07-19 22:13  刘宏缔的架构森林  阅读(5)  评论(0)    收藏  举报