FastAPI 权威指南:RBAC 权限设计与 JWT 实现深度解析

334 阅读6分钟

FastAPI 是一个现代、高性能的 Python Web 框架,广泛用于构建 RESTful API。它与 Python 的类型提示特性结合得非常好,开发效率高且易于维护。在实际应用中,权限管理和用户认证是 API 开发中的核心需求。

1. RBAC 权限设计简介

RBAC(Role-Based Access Control,基于角色的访问控制)是一种常见的权限管理模型。它的核心思想是将权限分配给角色(Role),然后将角色分配给用户(User)。通过这种方式,可以灵活地管理用户的访问权限。

1.1 RBAC 的核心组件

  • 用户(User):系统的操作主体。
  • 角色(Role):定义一组权限的集合,例如“管理员”、“编辑者”、“普通用户”。
  • 权限(Permission):具体的操作能力,例如“读取数据”、“删除记录”。
  • 用户-角色关系:用户被分配一个或多个角色。
  • 角色-权限关系:角色被分配一组权限。

1.2 RBAC 的优势

  • 灵活性:通过调整角色和权限的关系,可以快速更改用户的访问能力。
  • 可维护性:权限管理集中在角色层面,便于扩展和修改。
  • 安全性:限制用户只能访问其角色允许的资源。

在 FastAPI 中,我们将通过数据库存储用户、角色和权限的关系,并利用 JWT 实现认证和权限验证。

2. JWT 简介与工作原理

JWT(JSON Web Token)是一种基于 JSON 的开放标准,用于在网络中传递认证信息。它由三部分组成:

  • Header:包含令牌的类型和签名算法(如 HMAC SHA256)。
  • Payload:包含声明(Claims),如用户信息、角色、过期时间等。
  • Signature:使用密钥对 Header 和 Payload 进行签名,确保令牌未被篡改。

2.1 JWT 的典型流程

  1. 用户登录时,输入用户名和密码。
  2. 服务端验证凭据后,生成一个 JWT 并返回给客户端。
  3. 客户端在后续请求中携带 JWT(通常放在 HTTP 头的 Authorization 字段中)。
  4. 服务端验证 JWT 的有效性,并根据其中的信息判断用户的权限。

2.2 在 FastAPI 中使用 JWT 的好处

  • 无状态:服务端无需存储会话信息,适合分布式系统。
  • 安全性:通过签名确保令牌的完整性。
  • 灵活性:Payload 中可以携带自定义数据,例如用户的角色。

3. FastAPI 项目搭建与实现

下面我们将通过一个完整的示例,展示如何在 FastAPI 中实现 RBAC 和 JWT。

3.1 环境准备

首先,确保安装以下依赖:

pip install fastapi uvicorn python-jose[cryptography] passlib[bcrypt] sqlalchemy pydantic

项目结构如下:

project/
├── main.py           # 主应用文件
├── models.py        # 数据库模型
├── schemas.py       # Pydantic 模型
├── crud.py          # 数据库操作
├── auth.py          # 认证和 JWT 逻辑
└── database.py      # 数据库连接

3.2 数据库模型(models.py)

我们使用 SQLAlchemy 定义用户、角色和权限的表结构:

from sqlalchemy import Column, Integer, String, ForeignKey, Table
from sqlalchemy.orm import relationship
from database import Base

# 用户和角色的多对多关系表
user_role = Table(
    "user_role",
    Base.metadata,
    Column("user_id", Integer, ForeignKey("users.id")),
    Column("role_id", Integer, ForeignKey("roles.id")),
)

# 角色和权限的多对多关系表
role_permission = Table(
    "role_permission",
    Base.metadata,
    Column("role_id", Integer, ForeignKey("roles.id")),
    Column("permission_id", Integer, ForeignKey("permissions.id")),
)

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True)
    hashed_password = Column(String)
    roles = relationship("Role", secondary=user_role, back_populates="users")

class Role(Base):
    __tablename__ = "roles"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, unique=True, index=True)
    users = relationship("User", secondary=user_role, back_populates="roles")
    permissions = relationship("Permission", secondary=role_permission, back_populates="roles")

class Permission(Base):
    __tablename__ = "permissions"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, unique=True, index=True)
    roles = acceleration("Role", secondary=role_permission, back_populates="permissions")

3.3 Pydantic 模型(schemas.py)

定义用于输入和输出的数据模型:

from pydantic import BaseModel

class Token(BaseModel):
    access_token: str
    token_type: str

class UserCreate(BaseModel):
    username: str
    password: str

class User(BaseModel):
    idint
    username: str
    roles: list[str]

    class Config:
        orm_mode = True

3.4 数据库连接(database.py)

配置 SQLAlchemy 数据库连接:

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

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

3.5 认证与 JWT 逻辑(auth.py)

实现用户认证和 JWT 生成:

from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from models import User
from database import get_db

SECRET_KEY = "your-secret-key"  # 请使用安全的密钥
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password):
    return pwd_context.hash(password)

def create_access_token(data: dict, expires_delta: timedelta = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate""Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    user = db.query(User).filter(User.username == username).first()
    if user is None:
        raise credentials_exception
    return user

def check_permission(permission: str, user: User = Depends(get_current_user)):
    for role in user.roles:
        for perm in role.permissions:
            if perm.name == permission:
                return True
    raise HTTPException(status_code=403, detail="Permission denied")

3.6 主应用(main.py)

实现登录、受保护路由和权限检查:

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from auth import (
    verify_password, create_access_token, get_current_user, check_permission,
    ACCESS_TOKEN_EXPIRE_MINUTES, get_password_hash
)
from schemas import Token, UserCreate, User
from models import User as UserModel
from database import get_db
from crud import create_user

app = FastAPI()

@app.post("/token", response_model=Token)
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
    user = db.query(UserModel).filter(UserModel.username == form_data.username).first()
    if not user or not verify_password(form_data.password, user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate""Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type""bearer"}

@app.post("/users/", response_model=User)
def register_user(user: UserCreate, db: Session = Depends(get_db)):
    db_user = db.query(UserModel).filter(UserModel.username == user.username).first()
    if db_user:
        raise HTTPException(status_code=400, detail="Username already registered")
    hashed_password = get_password_hash(user.password)
    db_user = UserModel(username=user.username, hashed_password=hashed_password)
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user

@app.get("/admin/", response_model=User)
def admin_only(user: UserModel = Depends(lambda: check_permission("admin_access"))):
    return user

@app.get("/editor/", response_model=User)
def editor_only(user: UserModel = Depends(lambda: check_permission("edit_access"))):
    return user

3.7 CRUD 操作(crud.py)

简单实现用户创建逻辑:

from sqlalchemy.orm import Session
from models import User
from auth import get_password_hash

def create_user(db: Session, username: str, password: str):
    hashed_password = get_password_hash(password)
    db_user = User(username=username, hashed_password=hashed_password)
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user

4. 示例运行与测试

4.1 初始化数据库

运行以下代码创建表:

from database import Base, engine
Base.metadata.create_all(bind=engine)

4.2 启动应用

uvicorn main:app --reload

4.3 测试流程

4.3.1 注册用户:

curl -X POST "http://127.0.0.1:8000/users/" -H "Content-Type: application/json" -d '{"username": "admin", "password": "123456"}'

4.3.2 获取 Token:

curl -X POST "http://127.0.0.1:8000/token" -H "Content-Type: application/x-www-form-urlencoded" -d "username=admin&password=123456"

4.3.3 访问受保护路由

将返回的 access_token 放入请求头:

curl -GET "http://127.0.0.1:8000/admin/" -H "Authorization: Bearer <your-token>"

注意:需要在数据库中手动为用户分配角色和权限,才能通过权限检查。


5. 总结

通过上述实现,我们在 FastAPI 中结合 RBAC 和 JWT 构建了一个安全的权限管理系统:

  • RBAC:通过用户-角色-权限的模型实现灵活的权限分配。
  • JWT:实现无状态认证,确保 API 的安全性。
  • FastAPI:利用依赖注入和类型提示,代码简洁且易于维护。

在实际生产环境中,建议:

  • 使用更安全的 SECRET_KEY 并妥善存储。
  • 添加刷新 Token 机制以延长会话。
  • 对数据库操作进行更多的错误处理和日志记录。

6. 联系方式

感谢你看到这里,如果觉得文章对你有所收获,请在文末为我点个【赞】+【关注】,或者【转发】给身边更多有需要的人看,你的点赞就是对我莫大的支持与动力!