Skip to content

39|权限控制与依赖注入

认证解决"你是谁"的问题后,还需要解决"你能做什么"的问题。管理员可以删除服务器,普通用户只能查看;某些接口需要登录才能访问。FastAPI 的依赖注入系统(Dependency Injection)让权限校验、数据库会话获取、当前用户解析这些横切关注点从业务代码中抽离,统一复用。

一、依赖注入基础

依赖注入的核心是:路由处理函数声明自己需要什么,FastAPI 自动提供。这个"提供"的过程封装在依赖函数中。

python
from fastapi import Depends, HTTPException

# 依赖函数:获取数据库会话
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# 路由中使用依赖
@app.get("/servers")
async def list_servers(db: Session = Depends(get_db)):
    return db.execute(select(Server)).scalars().all()

Depends(get_db) 告诉 FastAPI:"这个参数不是来自请求,而是调用 get_db() 获取"。每次请求都会创建新的会话,请求结束后自动关闭。

依赖的依赖

依赖函数本身也可以依赖其他依赖:

python
async def get_current_user(token: str = Depends(oauth2_scheme)):
    payload = decode_token(token)
    return payload.get("sub")

async def get_current_admin(user_id: str = Depends(get_current_user)):
    user = get_user_by_id(user_id)
    if user.role != "admin":
        raise HTTPException(status_code=403, detail="无权限")
    return user

二、登录依赖

python
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/login")

async def require_user(token: str = Depends(oauth2_scheme)):
    try:
        payload = decode_token(token)
        user_id = payload.get("sub")
        if not user_id:
            raise HTTPException(status_code=401, detail="无效凭证")
        return user_id
    except ValueError:
        raise HTTPException(status_code=401, detail="凭证无效或已过期")

任何需要登录的接口都加上这个依赖:

python
@app.get("/servers")
async def list_servers(user_id: str = Depends(require_user)):
    return {"user_id": user_id, "servers": [...]}

未登录用户访问时,OAuth2PasswordBearer 自动返回 401,并在响应头中加入 WWW-Authenticate: Bearer

三、角色权限控制

基于角色的依赖

python
class RoleChecker:
    def __init__(self, allowed_roles: list[str]):
        self.allowed_roles = allowed_roles

    def __call__(self, user: User = Depends(get_current_user)):
        if user.role not in self.allowed_roles:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="权限不足",
            )
        return user

require_admin = RoleChecker(["admin"])
require_editor = RoleChecker(["admin", "editor"])

使用:

python
@app.delete("/servers/{id}")
async def delete_server(
    id: int,
    user: User = Depends(require_admin),
):
    return {"deleted": id}

@app.put("/servers/{id}")
async def update_server(
    id: int,
    user: User = Depends(require_editor),
):
    return {"updated": id}

接口级别的权限

更细粒度的控制可以在接口内部判断:

python
@app.post("/servers/{id}/tasks")
async def create_task(
    id: int,
    user: User = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    server = db.get(Server, id)
    if not server:
        raise HTTPException(status_code=404)

    # 只能给自己的服务器创建任务
    if server.owner_id != user.id and user.role != "admin":
        raise HTTPException(status_code=403)

    # 创建任务...

四、全局依赖

某些依赖应用于所有路由,如日志记录、请求计时:

python
async def log_requests(request: Request):
    start = time.time()
    yield
    duration = time.time() - start
    print(f"{request.method} {request.url.path} - {duration:.3f}s")

app = FastAPI(dependencies=[Depends(log_requests)])

yield 之前的代码在路由执行前运行,yield 之后的代码在路由执行后运行。这在依赖注入中称为"退出栈",适合日志、计时、事务管理。

五、依赖返回值的使用

依赖的返回值可以直接在路由函数中使用:

python
@app.get("/servers")
async def list_servers(
    user: User = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    # user 和 db 都已经通过依赖注入获取
    if user.role == "admin":
        return db.execute(select(Server)).scalars().all()
    else:
        return db.execute(
            select(Server).where(Server.owner_id == user.id)
        ).scalars().all()

六、常见错误

依赖函数没有用 yield

python
# 错误:没有 yield,FastAPI 不会正确处理清理逻辑
def get_db():
    db = SessionLocal()
    return db   # 会话不会自动关闭

# 正确
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

依赖中抛出的异常被吞掉

python
def get_db():
    db = SessionLocal()
    try:
        yield db
    except Exception:
        db.rollback()
        raise
    finally:
        db.close()

如果依赖中需要处理异常(如回滚事务),要在 yield 后用 try/except。但不要把所有异常都吞掉——应该 re-raise 让 FastAPI 的正常异常处理机制接管。

权限校验放在业务代码中而不是依赖中

python
# 不推荐:每个路由都重复写权限判断
@app.delete("/servers/{id}")
async def delete_server(id: int, user: User = Depends(get_current_user)):
    if user.role != "admin":
        raise HTTPException(status_code=403)
    ...

# 推荐:权限判断封装成依赖,路由声明式使用
@app.delete("/servers/{id}")
async def delete_server(id: int, user: User = Depends(require_admin)):
    ...

依赖返回值类型没注解

python
# 错误:没有类型注解,FastAPI 无法正确注入
def get_current_user(token: str = Depends(oauth2_scheme)):
    ...

# 正确:返回类型明确标注
def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    ...