diff --git a/backend/auth/__pycache__/config.cpython-313.pyc b/backend/auth/__pycache__/config.cpython-313.pyc index b5903ef..65b581c 100644 Binary files a/backend/auth/__pycache__/config.cpython-313.pyc and b/backend/auth/__pycache__/config.cpython-313.pyc differ diff --git a/backend/auth/__pycache__/database.cpython-313.pyc b/backend/auth/__pycache__/database.cpython-313.pyc index 3b87193..b9b0450 100644 Binary files a/backend/auth/__pycache__/database.cpython-313.pyc and b/backend/auth/__pycache__/database.cpython-313.pyc differ diff --git a/backend/auth/__pycache__/init_data.cpython-313.pyc b/backend/auth/__pycache__/init_data.cpython-313.pyc index ad1310d..501cd71 100644 Binary files a/backend/auth/__pycache__/init_data.cpython-313.pyc and b/backend/auth/__pycache__/init_data.cpython-313.pyc differ diff --git a/backend/auth/__pycache__/middleware.cpython-313.pyc b/backend/auth/__pycache__/middleware.cpython-313.pyc index f4e1bb5..46a02ad 100644 Binary files a/backend/auth/__pycache__/middleware.cpython-313.pyc and b/backend/auth/__pycache__/middleware.cpython-313.pyc differ diff --git a/backend/auth/__pycache__/models.cpython-313.pyc b/backend/auth/__pycache__/models.cpython-313.pyc index 6523be3..9ab9849 100644 Binary files a/backend/auth/__pycache__/models.cpython-313.pyc and b/backend/auth/__pycache__/models.cpython-313.pyc differ diff --git a/backend/auth/__pycache__/schemas.cpython-313.pyc b/backend/auth/__pycache__/schemas.cpython-313.pyc index ea91a5d..d8aee0c 100644 Binary files a/backend/auth/__pycache__/schemas.cpython-313.pyc and b/backend/auth/__pycache__/schemas.cpython-313.pyc differ diff --git a/backend/auth/apps/__pycache__/urls.cpython-313.pyc b/backend/auth/apps/__pycache__/urls.cpython-313.pyc index b51d6bc..73f9639 100644 Binary files a/backend/auth/apps/__pycache__/urls.cpython-313.pyc and b/backend/auth/apps/__pycache__/urls.cpython-313.pyc differ diff --git a/backend/auth/apps/permissions/__pycache__/urls.cpython-313.pyc b/backend/auth/apps/permissions/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000..308c4b6 Binary files /dev/null and b/backend/auth/apps/permissions/__pycache__/urls.cpython-313.pyc differ diff --git a/backend/auth/apps/permissions/urls.py b/backend/auth/apps/permissions/urls.py new file mode 100644 index 0000000..cdb9adb --- /dev/null +++ b/backend/auth/apps/permissions/urls.py @@ -0,0 +1,173 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from fastcrud import FastCRUD + +from database import get_db +from models import Permission, Role +from schemas import ( + PermissionCreate, + PermissionResponse, + RolePermissionAssign, + RolePermissionResponse, +) +from middleware import get_current_user +from models import User + +router = APIRouter(prefix="/permissions", tags=["permissions"]) + +# 创建FastCRUD实例 +permission_crud = FastCRUD(Permission, PermissionResponse) + + +@router.post("/", response_model=PermissionResponse) +async def create_permission( + permission_data: PermissionCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """创建权限""" + # 检查权限编码是否已存在 + result = await db.execute(select(Permission).filter(Permission.code == permission_data.code)) + existing_permission = result.scalar_one_or_none() + if existing_permission: + raise HTTPException(status_code=400, detail="权限编码已存在") + + # 创建权限实例 + db_permission = Permission( + name=permission_data.name, + code=permission_data.code, + description=permission_data.description, + ) + + db.add(db_permission) + await db.commit() + await db.refresh(db_permission) + + return db_permission + + +@router.get("/", response_model=list[PermissionResponse]) +async def get_permissions( + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """获取所有权限""" + result = await db.execute(select(Permission)) + permissions = result.scalars().all() + return permissions + + +@router.get("/{permission_id}", response_model=PermissionResponse) +async def get_permission( + permission_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """获取单个权限""" + permission = await permission_crud.get(db, id=permission_id) + if not permission: + raise HTTPException(status_code=404, detail="权限不存在") + return permission + + +@router.put("/{permission_id}", response_model=PermissionResponse) +async def update_permission( + permission_id: int, + permission_data: PermissionCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """更新权限""" + # 检查权限编码是否被其他权限使用 + result = await db.execute( + select(Permission).filter(Permission.code == permission_data.code, Permission.id != permission_id) + ) + existing_permission = result.scalar_one_or_none() + if existing_permission: + raise HTTPException(status_code=400, detail="权限编码已存在") + + # 准备更新数据 + update_data = permission_data.model_dump() + + # 使用FastCRUD更新权限 + updated_permission = await permission_crud.update( + db, update_data, id=permission_id, schema_to_select=PermissionResponse, return_as_model=True + ) + if not updated_permission: + raise HTTPException(status_code=404, detail="权限不存在") + return updated_permission + + +@router.delete("/{permission_id}") +async def delete_permission( + permission_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """删除权限""" + # 检查权限是否存在 + result = await db.execute(select(Permission).filter(Permission.id == permission_id)) + existing_permission = result.scalar_one_or_none() + if not existing_permission: + raise HTTPException(status_code=404, detail="权限不存在") + + # 使用FastCRUD删除权限 + deleted = await permission_crud.delete(db, id=permission_id) + return {"message": "权限删除成功"} + + +# 角色权限关联接口 +@router.post("/assign", response_model=RolePermissionResponse) +async def assign_permissions_to_role( + data: RolePermissionAssign, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """为角色分配权限""" + # 查找角色 + result = await db.execute(select(Role).filter(Role.id == data.role_id)) + role = result.scalar_one_or_none() + if not role: + raise HTTPException(status_code=404, detail="角色不存在") + + # 查找要分配的权限 + permissions = [] + for permission_id in data.permission_ids: + result = await db.execute(select(Permission).filter(Permission.id == permission_id)) + permission = result.scalar_one_or_none() + if permission: + permissions.append(permission) + + # 更新角色的权限 + role.permissions = permissions + await db.commit() + await db.refresh(role) + + # 返回角色权限信息 + return RolePermissionResponse( + role_id=role.id, + role_name=role.name, + permissions=[PermissionResponse.from_orm(p) for p in role.permissions] + ) + + +@router.get("/role/{role_id}", response_model=RolePermissionResponse) +async def get_role_permissions( + role_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """获取角色的权限列表""" + # 查找角色 + result = await db.execute(select(Role).filter(Role.id == role_id)) + role = result.scalar_one_or_none() + if not role: + raise HTTPException(status_code=404, detail="角色不存在") + + # 返回角色权限信息 + return RolePermissionResponse( + role_id=role.id, + role_name=role.name, + permissions=[PermissionResponse.from_orm(p) for p in role.permissions] + ) \ No newline at end of file diff --git a/backend/auth/apps/urls.py b/backend/auth/apps/urls.py index d326d7a..fd4e2ee 100644 --- a/backend/auth/apps/urls.py +++ b/backend/auth/apps/urls.py @@ -1,10 +1,12 @@ from fastapi import APIRouter from .users.urls import router as users_router from .roles.urls import router as roles_router +from .permissions.urls import router as permissions_router # 创建主路由 api_router = APIRouter() # 包含子路由 api_router.include_router(users_router) -api_router.include_router(roles_router) \ No newline at end of file +api_router.include_router(roles_router) +api_router.include_router(permissions_router) \ No newline at end of file diff --git a/backend/auth/booksystem.db b/backend/auth/booksystem.db new file mode 100644 index 0000000..e67a57a Binary files /dev/null and b/backend/auth/booksystem.db differ diff --git a/backend/auth/database.py b/backend/auth/database.py index 14f2350..188d29c 100644 --- a/backend/auth/database.py +++ b/backend/auth/database.py @@ -23,4 +23,4 @@ async def get_db(): try: yield session finally: - await session.close() \ No newline at end of file + await session.close() diff --git a/backend/auth/init_data.py b/backend/auth/init_data.py index 2229e6a..6afc64b 100644 --- a/backend/auth/init_data.py +++ b/backend/auth/init_data.py @@ -1,9 +1,116 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select -from models import Role, User +from models import Role, User, Permission from utils.password import get_password_hash +async def init_permissions(db: AsyncSession): + """初始化权限数据""" + # 定义需要初始化的权限 + required_permissions = [ + {"name": "用户管理", "code": "user:manage", "description": "用户管理权限"}, + {"name": "用户查询", "code": "user:read", "description": "用户查询权限"}, + {"name": "用户创建", "code": "user:create", "description": "用户创建权限"}, + {"name": "用户更新", "code": "user:update", "description": "用户更新权限"}, + {"name": "用户删除", "code": "user:delete", "description": "用户删除权限"}, + {"name": "角色管理", "code": "role:manage", "description": "角色管理权限"}, + {"name": "角色查询", "code": "role:read", "description": "角色查询权限"}, + {"name": "角色创建", "code": "role:create", "description": "角色创建权限"}, + {"name": "角色更新", "code": "role:update", "description": "角色更新权限"}, + {"name": "角色删除", "code": "role:delete", "description": "角色删除权限"}, + {"name": "权限管理", "code": "permission:manage", "description": "权限管理权限"}, + {"name": "权限查询", "code": "permission:read", "description": "权限查询权限"}, + {"name": "权限创建", "code": "permission:create", "description": "权限创建权限"}, + {"name": "权限更新", "code": "permission:update", "description": "权限更新权限"}, + {"name": "权限删除", "code": "permission:delete", "description": "权限删除权限"}, + ] + + # 检查现有权限 + result = await db.execute(select(Permission)) + existing_permissions = result.scalars().all() + existing_permission_codes = {perm.code for perm in existing_permissions} + + # 检查需要创建或更新的权限 + permissions_to_add = [] + permissions_to_update = [] + + for perm_data in required_permissions: + perm_code = perm_data["code"] + if perm_code not in existing_permission_codes: + # 权限不存在,需要创建 + permissions_to_add.append(Permission(**perm_data)) + else: + # 权限存在,检查是否需要更新 + existing_perm = next( + perm for perm in existing_permissions if perm.code == perm_code + ) + if existing_perm.name != perm_data["name"] or existing_perm.description != perm_data["description"]: + existing_perm.name = perm_data["name"] + existing_perm.description = perm_data["description"] + permissions_to_update.append(existing_perm) + + # 执行创建和更新操作 + if permissions_to_add: + for perm in permissions_to_add: + db.add(perm) + await db.commit() + print(f"创建了 {len(permissions_to_add)} 个权限") + + if permissions_to_update: + await db.commit() + print(f"更新了 {len(permissions_to_update)} 个权限") + + if not permissions_to_add and not permissions_to_update: + print("权限数据已存在且无需更新,跳过初始化") + + print("权限数据初始化/更新完成") + + +async def init_role_permissions(db: AsyncSession): + """初始化角色权限分配""" + from sqlalchemy.orm import selectinload + + # 获取管理员角色(使用预加载避免懒加载问题) + result = await db.execute( + select(Role) + .filter(Role.name == "管理员") + .options(selectinload(Role.permissions)) + ) + admin_role = result.scalar_one_or_none() + if not admin_role: + print("管理员角色不存在,请先初始化角色数据") + return + + # 获取所有权限 + result = await db.execute(select(Permission)) + all_permissions = result.scalars().all() + + if not all_permissions: + print("没有找到任何权限,请先初始化权限数据") + return + + # 获取管理员角色当前的权限(使用预加载的数据) + admin_permission_codes = set() + if admin_role.permissions: + admin_permission_codes = {perm.code for perm in admin_role.permissions} + + # 检查是否需要添加权限 + permissions_to_add = [] + for perm in all_permissions: + if perm.code not in admin_permission_codes: + permissions_to_add.append(perm) + + if permissions_to_add: + # 直接设置权限(替换而非追加,确保权限完整) + admin_role.permissions = all_permissions + await db.commit() + print(f"为管理员角色分配了 {len(permissions_to_add)} 个权限") + else: + print("管理员角色权限已完整,无需更新") + + print("角色权限初始化/更新完成") + + async def init_roles(db: AsyncSession): """初始化角色数据""" # 定义需要初始化的角色 @@ -187,6 +294,8 @@ async def init_all_data(db: AsyncSession): """初始化所有数据""" print("开始初始化数据库数据...") await init_roles(db) + await init_permissions(db) + await init_role_permissions(db) await init_admin_user(db) await init_test_user(db) print("数据库数据初始化完成") diff --git a/backend/auth/middleware.py b/backend/auth/middleware.py index e91864a..fed45b6 100644 --- a/backend/auth/middleware.py +++ b/backend/auth/middleware.py @@ -3,6 +3,7 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select import redis.asyncio as redis +from typing import Callable from database import get_db from utils.jwt import decode_token @@ -49,3 +50,108 @@ async def get_current_user( raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在") return user + + +def has_permission(required_permission_code: str) -> Callable: + """ + 权限验证依赖函数工厂 + + 使用示例: + @router.get("/protected", dependencies=[Depends(has_permission("user:read"))]) + async def protected_route(): + ... + """ + async def permission_checker( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), + ): + # 获取用户角色的所有权限 + from models import Permission, Role + + result = await db.execute( + select(Permission) + .join(Role.permissions) + .filter(Role.id == current_user.role_id) + ) + permissions = result.scalars().all() + + # 检查是否有 required_permission_code 权限 + has_perm = any(perm.code == required_permission_code for perm in permissions) + if not has_perm: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"没有访问权限,需要权限: {required_permission_code}", + ) + return current_user + + return permission_checker + + +def has_any_permission(required_permission_codes: list[str]) -> Callable: + """ + 多权限验证依赖函数工厂(满足任一权限即可) + + 使用示例: + @router.get("/protected", dependencies=[Depends(has_any_permission(["user:read", "admin:read"]))]) + async def protected_route(): + ... + """ + async def permission_checker( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), + ): + from models import Permission, Role + + result = await db.execute( + select(Permission) + .join(Role.permissions) + .filter(Role.id == current_user.role_id) + ) + permissions = result.scalars().all() + user_permission_codes = {perm.code for perm in permissions} + + # 检查是否有任一需要的权限 + has_perm = any(code in user_permission_codes for code in required_permission_codes) + if not has_perm: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"没有访问权限,需要以下任一权限: {', '.join(required_permission_codes)}", + ) + return current_user + + return permission_checker + + +def has_all_permissions(required_permission_codes: list[str]) -> Callable: + """ + 多权限验证依赖函数工厂(需要满足所有权限) + + 使用示例: + @router.get("/protected", dependencies=[Depends(has_all_permissions(["user:read", "user:write"]))]) + async def protected_route(): + ... + """ + async def permission_checker( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), + ): + from models import Permission, Role + + result = await db.execute( + select(Permission) + .join(Role.permissions) + .filter(Role.id == current_user.role_id) + ) + permissions = result.scalars().all() + user_permission_codes = {perm.code for perm in permissions} + + # 检查是否满足所有需要的权限 + missing_permissions = [code for code in required_permission_codes if code not in user_permission_codes] + if missing_permissions: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"缺少必要权限: {', '.join(missing_permissions)}", + ) + return current_user + + return permission_checker diff --git a/backend/auth/models.py b/backend/auth/models.py index af3cf33..420e7e5 100644 --- a/backend/auth/models.py +++ b/backend/auth/models.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func, Table from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declarative_base from datetime import datetime @@ -20,6 +20,12 @@ class Role(Base): # 关联到User users = relationship("User", back_populates="role_rel") + # 关联到Permission(多对多) + permissions = relationship( + "Permission", + secondary="role_permission", + back_populates="roles" + ) class User(Base): @@ -43,3 +49,33 @@ class User(Base): # 关联到Role role_rel = relationship("Role", back_populates="users") + + +class Permission(Base): + """权限数据库模型""" + + __tablename__ = "permission" + id = Column(Integer, primary_key=True, index=True, comment="权限ID") + name = Column(String(50), nullable=False, comment="权限名称") + code = Column(String(50), nullable=False, unique=True, comment="权限编码") + description = Column(String(255), nullable=True, comment="权限描述") + createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间") + updatetime = Column( + DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间" + ) + + # 关联到Role(多对多) + roles = relationship( + "Role", + secondary="role_permission", + back_populates="permissions" + ) + + +# 角色权限关联表(多对多) +role_permission = Table( + "role_permission", + Base.metadata, + Column("role_id", Integer, ForeignKey("role.id"), primary_key=True, comment="角色ID"), + Column("permission_id", Integer, ForeignKey("permission.id"), primary_key=True, comment="权限ID") +) diff --git a/backend/auth/schemas.py b/backend/auth/schemas.py index 19c40f5..36e4dba 100644 --- a/backend/auth/schemas.py +++ b/backend/auth/schemas.py @@ -106,3 +106,44 @@ class RoleResponse(RoleBase): class Config: from_attributes = True + + +# 权限相关模型 +class PermissionBase(BaseModel): + """权限基础模型""" + + name: str = Field(..., max_length=50, description="权限名称") + code: str = Field(..., max_length=50, description="权限编码") + description: Optional[str] = Field(None, max_length=255, description="权限描述") + + +class PermissionCreate(PermissionBase): + """权限创建模型""" + pass + + +class PermissionResponse(PermissionBase): + """权限响应模型""" + + id: int = Field(..., description="权限ID") + createtime: datetime = Field(..., description="创建时间") + updatetime: datetime = Field(..., description="更新时间") + + class Config: + from_attributes = True + + +# 角色权限关联相关模型 +class RolePermissionAssign(BaseModel): + """角色权限分配模型""" + + role_id: int = Field(..., description="角色ID") + permission_ids: list[int] = Field(..., description="权限ID列表") + + +class RolePermissionResponse(BaseModel): + """角色权限响应模型""" + + role_id: int = Field(..., description="角色ID") + role_name: str = Field(..., description="角色名称") + permissions: list[PermissionResponse] = Field(..., description="权限列表")