diff --git a/backend/controller/role.py b/backend/controller/role.py index d9e1222..6579935 100644 --- a/backend/controller/role.py +++ b/backend/controller/role.py @@ -4,15 +4,10 @@ from fastapi import Query from core.utils import list_to_tree from dbhelper.relation import role_assigned_menu -from dbhelper.role import ( - del_role, - get_role, - get_role_menus, - get_roles, - new_role, - put_role, -) -from schemas import ListAll, Response, RoleIn, RoleInfo, RoleMenuIn, RoleQuery, RoleRead +from dbhelper.role import (del_role, get_role, get_role_menus, get_roles, + new_role, put_role) +from schemas import (ListAll, Response, RoleIn, RoleInfo, RoleMenuIn, + RoleQuery, RoleRead) async def role_add(data: RoleIn) -> Response[RoleInfo]: diff --git a/backend/controller/user.py b/backend/controller/user.py index 7a6003b..81e9647 100644 --- a/backend/controller/user.py +++ b/backend/controller/user.py @@ -1,14 +1,8 @@ from fastapi import Query from core.security import get_password_hash -from dbhelper.user import ( - del_user, - get_user, - get_user_info, - get_users, - insert_user, - put_user, -) +from dbhelper.user import (del_user, get_user, get_user_info, get_users, + insert_user, put_user) from schemas import Response, UserAdd, UserInfo, UserPut, UserQuery, UserRead from schemas.common import ListAll diff --git a/backend/core/exceptions.py b/backend/core/exceptions.py index dcd0c5e..cc05e55 100644 --- a/backend/core/exceptions.py +++ b/backend/core/exceptions.py @@ -1,6 +1,23 @@ from fastapi.exceptions import HTTPException +from starlette.requests import Request +from starlette.responses import JSONResponse class TokenAuthFailure(HTTPException): - status_code = 401 - detail = "认证失败" + + pass + + +class PermissionsError(HTTPException): + pass + + +async def http_exception(request: Request, exc: HTTPException): + return JSONResponse( + {"msg": exc.detail, "code": exc.status_code, "data": None}, + status_code=exc.status_code, + headers=exc.headers, + ) + + +exception_handlers = {HTTPException: http_exception} diff --git a/backend/core/security.py b/backend/core/security.py index c56d9f2..c178d0d 100644 --- a/backend/core/security.py +++ b/backend/core/security.py @@ -1,13 +1,15 @@ from datetime import datetime, timedelta from typing import Optional -from fastapi import Depends +from fastapi import Depends, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from jose import JWTError, jwt from passlib.context import CryptContext -from core.exceptions import TokenAuthFailure -from dbhelper.user import get_user +from core.exceptions import PermissionsError, TokenAuthFailure +from dbhelper.menu import get_apis, get_has_api +from dbhelper.user import get_user, get_user_info +from models import UserModel # JWT SECRET_KEY = "lLNiBWPGiEmCLLR9kRGidgLY7Ac1rpSWwfGzTJpTmCU" @@ -59,4 +61,33 @@ async def check_token(security: HTTPAuthorizationCredentials = Depends(bearer)): username: str = payload.get("sub") return await get_user({"username": username}) except JWTError: - raise TokenAuthFailure + raise TokenAuthFailure(403, "认证失败") + + +async def check_permissions(request: Request, user: UserModel = Depends(check_token)): + """检查接口权限""" + # 查询当前激活角色 + result = await get_user_info(user) + active_rid = result["roles"][0]["id"] + + # 白名单 + whitelist = [f"/user/{user.id}", f"/role/{active_rid}/menu"] + flag = request.url.path in whitelist and request.method == "GET" + if flag: + return + + api = request.url.path + for k, v in request.path_params.items(): + api = api.replace(v, "{%s}" % k) + # 方法1. 每一次去查数据库 + # result = await get_has_api(active_rid, api, request.method) + + # 2. 登录之后查一次 后面去结果查 todo 更新权限时需要更新 , 最好结果放redis + cache_key = f"{user.username}_{active_rid}" + # 缓存到fastapi 应用实例中 + if not hasattr(request.app.state, cache_key): + setattr(request.app.state, cache_key, await get_apis(active_rid)) + if {"api": api, "method": request.method} not in getattr( + request.app.state, cache_key + ): + raise PermissionsError(403, detail="无权访问") diff --git a/backend/dbhelper/menu.py b/backend/dbhelper/menu.py index 1695c61..2c0c37b 100644 --- a/backend/dbhelper/menu.py +++ b/backend/dbhelper/menu.py @@ -1,3 +1,5 @@ +from tortoise import connections + from models import MenuModel from schemas.menu import MenuIn @@ -41,3 +43,27 @@ async def get_menu(kwargs): async def del_menu(mid: int): """删除用户""" return await MenuModel.filter(id=mid).update(status=9) + + +async def get_has_api(pk: int, api: str, method: str): + """获取角色接口权限 每次来查数据库""" + db = connections.get("default") + return await db.execute_query_dict( + """ + select m.api, m.method + FROM sys_menu as m, sys_role_menu as srm WHERE m.id = srm.mid + AND srm.rid = (?) and m.api = (?) and m.method = (?) and m.status != 9""", + [pk, api, method], + ) + + +async def get_apis(pk: int): + """返回当前角色拥有的接口权限列表""" + db = connections.get("default") + return await db.execute_query_dict( + """ + select m.api, m.method + FROM sys_menu as m, sys_role_menu as srm WHERE m.id = srm.mid + AND srm.rid = (?) and m.status != 9""", + [pk], + ) diff --git a/backend/dbhelper/role.py b/backend/dbhelper/role.py index 5fab1fe..9af9309 100644 --- a/backend/dbhelper/role.py +++ b/backend/dbhelper/role.py @@ -11,7 +11,7 @@ async def get_role_menus(rid: int): db = connections.get("default") return await db.execute_query_dict( """ - select m.id, m.name, m.meta, m.path, m.type, m.component, m.pid, m.identifier, m.regx,m.api, m.method + select m.id, m.name, m.meta, m.path, m.type, m.component, m.pid, m.identifier FROM sys_menu as m, sys_role_menu WHERE m.id = sys_role_menu.mid AND sys_role_menu.rid = (?) AND m.`status` = 1""", [rid], diff --git a/backend/main.py b/backend/main.py index 42fd74d..889a69d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from core.events import close_orm, init_orm +from core.exceptions import exception_handlers from core.log import logger from core.middleware import middlewares from router.url import routes @@ -10,14 +11,13 @@ app = FastAPI( on_shutdown=[close_orm], routes=routes, middleware=middlewares, + exception_handlers=exception_handlers, ) if __name__ == "__main__": import uvicorn for i in app.routes: - logger.info( - f"{i.path}, {i.methods}, {i.path_regex}, {i.__dict__.get('summary')}, {i.endpoint}" - ) + logger.info(f"{i.path}, {i.methods}, {i.__dict__.get('summary')}, {i.endpoint}") uvicorn.run("main:app", reload=True) diff --git a/backend/mini.db-shm b/backend/mini.db-shm index cb9b421..80e612c 100644 Binary files a/backend/mini.db-shm and b/backend/mini.db-shm differ diff --git a/backend/mini.db-wal b/backend/mini.db-wal index b85de46..09bee7e 100644 Binary files a/backend/mini.db-wal and b/backend/mini.db-wal differ diff --git a/backend/models/menu.py b/backend/models/menu.py index 4f7e57b..e36676f 100644 --- a/backend/models/menu.py +++ b/backend/models/menu.py @@ -15,7 +15,6 @@ class MenuModel(Table): identifier = fields.CharField(max_length=30, description="权限标识 user:add", null=True) api = fields.CharField(max_length=128, description="接口地址", null=True) method = fields.CharField(max_length=10, description="接口请求方式", null=True) - regx = fields.CharField(max_length=50, description="接口地址正则表达式", null=True) class Meta: table = "sys_menu" diff --git a/backend/router/url.py b/backend/router/url.py index ef6784b..631b753 100644 --- a/backend/router/url.py +++ b/backend/router/url.py @@ -4,17 +4,11 @@ from fastapi import Depends, routing from controller.common import about, login from controller.menu import menu_add, menu_arr, menu_del -from controller.role import ( - assigned_menu, - role_add, - role_arr, - role_del, - role_has_menu, - role_put, - role_query, -) -from controller.user import user_add, user_arr, user_del, user_info, user_list, user_put -from core.security import check_token +from controller.role import (assigned_menu, role_add, role_arr, role_del, + role_has_menu, role_put, role_query) +from controller.user import (user_add, user_arr, user_del, user_info, + user_list, user_put) +from core.security import check_permissions class Route(routing.APIRoute): @@ -113,48 +107,55 @@ class Route(routing.APIRoute): ) +has_perm = {"dependencies": [Depends(check_permissions)]} + routes = [ Route.post("/login", endpoint=login, tags=["公共"], summary="登录"), - Route.get("/about", endpoint=about, tags=["公共"], summary="关于"), + Route.get("/about", endpoint=about, tags=["公共"], summary="关于", **has_perm), # 用户管理 - Route.get("/user", endpoint=user_arr, tags=["用户管理"], summary="用户列表"), - Route.post("/user", endpoint=user_add, tags=["用户管理"], summary="用户新增"), + Route.get("/user", endpoint=user_arr, tags=["用户管理"], summary="用户列表", **has_perm), + Route.post("/user", endpoint=user_add, tags=["用户管理"], summary="用户新增", **has_perm), Route.delete( - "/user/{pk}", - endpoint=user_del, - tags=["用户管理"], - summary="用户删除", + "/user/{pk}", endpoint=user_del, tags=["用户管理"], summary="用户删除", **has_perm ), - Route.put("/user/{pk}", endpoint=user_put, tags=["用户管理"], summary="用户更新"), - Route.get("/user/{pk}", endpoint=user_info, tags=["用户管理"], summary="用户信息"), - Route.post("/user/query", endpoint=user_list, tags=["用户管理"], summary="用户列表查询"), - # 角色管理, - Route.get("/role", endpoint=role_arr, tags=["角色管理"], summary="角色列表"), - Route.post("/role", endpoint=role_add, tags=["角色管理"], summary="角色新增"), - Route.delete( - "/role/{pk}", - endpoint=role_del, - tags=["角色管理"], - summary="角色删除", - dependencies=[Depends(check_token)], + Route.put( + "/user/{pk}", endpoint=user_put, tags=["用户管理"], summary="用户更新", **has_perm ), Route.get( - "/role/{rid}/menu", endpoint=role_has_menu, tags=["角色管理"], summary="查询角色拥有权限" + "/user/{pk}", endpoint=user_info, tags=["用户管理"], summary="用户信息", **has_perm ), - Route.put("/role", endpoint=role_put, tags=["角色管理"], summary="角色更新"), - Route.post("/role/query", endpoint=role_query, tags=["角色管理"], summary="角色条件查询"), Route.post( - "/role/assigned/menu", endpoint=assigned_menu, tags=["角色管理"], summary="角色分配菜单" + "/user/query", endpoint=user_list, tags=["用户管理"], summary="用户列表查询", **has_perm + ), + # 角色管理, + Route.get("/role", endpoint=role_arr, tags=["角色管理"], summary="角色列表", **has_perm), + Route.post("/role", endpoint=role_add, tags=["角色管理"], summary="角色新增", **has_perm), + Route.delete( + "/role/{pk}", endpoint=role_del, tags=["角色管理"], summary="角色删除", **has_perm + ), + Route.get( + "/role/{rid}/menu", + endpoint=role_has_menu, + tags=["角色管理"], + summary="查询角色拥有权限", + **has_perm + ), + Route.put("/role", endpoint=role_put, tags=["角色管理"], summary="角色更新", **has_perm), + Route.post( + "/role/query", endpoint=role_query, tags=["角色管理"], summary="角色条件查询", **has_perm + ), + Route.post( + "/role/assigned/menu", + endpoint=assigned_menu, + tags=["角色管理"], + summary="角色分配菜单", + **has_perm ), # 菜单新增 - Route.get("/menu", endpoint=menu_arr, tags=["菜单管理"], summary="菜单列表"), - Route.post("/menu", endpoint=menu_add, tags=["菜单管理"], summary="菜单新增"), + Route.get("/menu", endpoint=menu_arr, tags=["菜单管理"], summary="菜单列表", **has_perm), + Route.post("/menu", endpoint=menu_add, tags=["菜单管理"], summary="菜单新增", **has_perm), Route.delete( - "/menu/{pk}", - endpoint=menu_del, - tags=["菜单管理"], - summary="菜单删除", - dependencies=[Depends(check_token)], + "/menu/{pk}", endpoint=menu_del, tags=["菜单管理"], summary="菜单删除", **has_perm ), ] diff --git a/backend/schemas/menu.py b/backend/schemas/menu.py index 707670e..f8dcb58 100644 --- a/backend/schemas/menu.py +++ b/backend/schemas/menu.py @@ -15,7 +15,6 @@ class MenuBasic(BaseModel): identifier: Optional[str] = Field(default=None, description="权限标识符 -> 按钮显示") api: Optional[str] = Field(default=None, description="后端接口地址") method: Optional[str] = Field(default=None, description="接口请求方法") - regx: Optional[str] = Field(default=None, description="正则匹配") class MenuIn(MenuBasic): diff --git a/backend/tests/test_case.py b/backend/tests/test_case.py index 2ea916d..1e66d2f 100644 --- a/backend/tests/test_case.py +++ b/backend/tests/test_case.py @@ -51,7 +51,6 @@ params = [ identifier=None, api=None, method=None, - regx=None, ).dict(), ), ( @@ -66,7 +65,6 @@ params = [ identifier=None, api=None, method=None, - regx=None, ).dict(), ), # 组件 @@ -81,8 +79,7 @@ params = [ pid=1, identifier=None, api="/user", - method="{'GET'}", - regx="^/user$", + method="GET", ).dict(), ), ( @@ -96,8 +93,7 @@ params = [ pid=1, identifier=None, api="/role", - method="{'GET'}", - regx="^/role$", + method="GET", ).dict(), ), ( @@ -111,8 +107,7 @@ params = [ pid=1, identifier=None, api="/menu", - method="{'GET'}", - regx="^/menu$", + method="GET", ).dict(), ), ( @@ -126,8 +121,7 @@ params = [ pid=2, identifier=None, api="/about", - method="{'GET'}", - regx="^/about", + method="GET", ).dict(), ), # 按钮 @@ -142,8 +136,7 @@ params = [ pid=3, identifier="user:create", api="/user", - method="{'POST'}", - regx="^/user$", + method="POST", ).dict(), ), ( @@ -157,8 +150,7 @@ params = [ pid=3, identifier="user:delete", api="/user/{pk}", - method="{'DELETE'}", - regx="^/user/(?P[^/]+)$", + method="DELETE", ).dict(), ), ( @@ -172,8 +164,7 @@ params = [ pid=3, identifier="user:update", api="/user/{pk}", - method="{'PUT'}", - regx="^/user/(?P[^/]+)$", + method="PUT", ).dict(), ), ( @@ -187,8 +178,7 @@ params = [ pid=3, identifier="user:get", api="/user/{pk}", - method="{'GET'}", - regx="^/user/(?P[^/]+)$", + method="GET", ).dict(), ), ( @@ -202,8 +192,7 @@ params = [ pid=3, identifier="user:query", api="/user/query", - method="{'POST'}", - regx="^/user/query$", + method="POST", ).dict(), ), # 角色管理 @@ -218,8 +207,7 @@ params = [ pid=4, identifier="role:create", api="/role", - method="{'POST'}", - regx="^/role$", + method="POST", ).dict(), ), ( @@ -233,8 +221,7 @@ params = [ pid=4, identifier="role:delete", api="/role/{pk}", - method="{'DELETE'}", - regx="^/role/(?P[^/]+)$", + method="DELETE", ).dict(), ), ( @@ -248,8 +235,7 @@ params = [ pid=4, identifier=None, api="/role/{rid}/menu", - method="{'GET'}", - regx="^/role/(?P[^/]+)/menu$", + method="GET", ).dict(), ), ( @@ -263,8 +249,7 @@ params = [ pid=4, identifier="", api="/role/query", - method="{'POST'}", - regx="^/role/query$", + method="POST", ).dict(), ), ( @@ -278,8 +263,7 @@ params = [ pid=4, identifier="role:assign", api="/role/assigned/menu", - method="{'POST'}", - regx="^/role/assigned/menu$", + method="POST", ).dict(), ), ( @@ -293,8 +277,7 @@ params = [ pid=4, identifier="role:update", api="/role", - method="{'PUT'}", - regx="^/role$", + method="PUT", ).dict(), ), # 菜单管理的权限 @@ -309,8 +292,7 @@ params = [ pid=5, identifier="menu:create", api="/menu", - method="{'POST'}", - regx="^/menu$", + method="POST", ).dict(), ), ( @@ -324,8 +306,7 @@ params = [ pid=5, identifier="menu:delete", api="/menu/{pk}", - method="{'DELETE'}", - regx="/menu/(?P[^/]+)$", + method="DELETE", ).dict(), ), # 分配权限 @@ -333,7 +314,7 @@ params = [ "/role/assigned/menu", RoleMenuIn(rid=1, menus=[num for num in range(1, 20)]).dict(), ), - ("/role/assigned/menu", RoleMenuIn(rid=2, menus=[3, 7, 8, 9, 10, 11]).dict()), + ("/role/assigned/menu", RoleMenuIn(rid=2, menus=[1, 3, 7, 8, 9, 11]).dict()), ] diff --git a/frontend/src/stores/user.js b/frontend/src/stores/user.js index 635b81d..1b14eb2 100644 --- a/frontend/src/stores/user.js +++ b/frontend/src/stores/user.js @@ -12,6 +12,14 @@ export const userStore = defineStore('user', () => { // getter const accessToken = computed(() => 'Bearer ' + token.value) + // setup store 不提供$reset 需要自己重置 + // https://github.com/vuejs/pinia/issues/1056 + const $reset = () => { + token.value = "" + userInfo.value = {} + userMenus.value = [] + } + // 非setup语法时的actions const loginAction = async (data) => { @@ -34,7 +42,8 @@ export const userStore = defineStore('user', () => { ElMessage.success("登录成功.") } - return { token, accessToken, userInfo, userMenus, loginAction } + return { token, accessToken, userInfo, userMenus, + $reset, loginAction } }, { persist: true, // 解决pinia刷新时数据丢失问题 }) diff --git a/frontend/src/views/main.vue b/frontend/src/views/main.vue index 55ca234..fc4161d 100644 --- a/frontend/src/views/main.vue +++ b/frontend/src/views/main.vue @@ -1,5 +1,12 @@