feat: api visit auth

This commit is contained in:
zy7y 2022-09-13 13:31:15 +08:00
parent fc1acab2d5
commit 9ce271d691
15 changed files with 170 additions and 109 deletions

View File

@ -4,15 +4,10 @@ from fastapi import Query
from core.utils import list_to_tree from core.utils import list_to_tree
from dbhelper.relation import role_assigned_menu from dbhelper.relation import role_assigned_menu
from dbhelper.role import ( from dbhelper.role import (del_role, get_role, get_role_menus, get_roles,
del_role, new_role, put_role)
get_role, from schemas import (ListAll, Response, RoleIn, RoleInfo, RoleMenuIn,
get_role_menus, RoleQuery, RoleRead)
get_roles,
new_role,
put_role,
)
from schemas import ListAll, Response, RoleIn, RoleInfo, RoleMenuIn, RoleQuery, RoleRead
async def role_add(data: RoleIn) -> Response[RoleInfo]: async def role_add(data: RoleIn) -> Response[RoleInfo]:

View File

@ -1,14 +1,8 @@
from fastapi import Query from fastapi import Query
from core.security import get_password_hash from core.security import get_password_hash
from dbhelper.user import ( from dbhelper.user import (del_user, get_user, get_user_info, get_users,
del_user, insert_user, put_user)
get_user,
get_user_info,
get_users,
insert_user,
put_user,
)
from schemas import Response, UserAdd, UserInfo, UserPut, UserQuery, UserRead from schemas import Response, UserAdd, UserInfo, UserPut, UserQuery, UserRead
from schemas.common import ListAll from schemas.common import ListAll

View File

@ -1,6 +1,23 @@
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import JSONResponse
class TokenAuthFailure(HTTPException): 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}

View File

@ -1,13 +1,15 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional from typing import Optional
from fastapi import Depends from fastapi import Depends, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt from jose import JWTError, jwt
from passlib.context import CryptContext from passlib.context import CryptContext
from core.exceptions import TokenAuthFailure from core.exceptions import PermissionsError, TokenAuthFailure
from dbhelper.user import get_user from dbhelper.menu import get_apis, get_has_api
from dbhelper.user import get_user, get_user_info
from models import UserModel
# JWT # JWT
SECRET_KEY = "lLNiBWPGiEmCLLR9kRGidgLY7Ac1rpSWwfGzTJpTmCU" SECRET_KEY = "lLNiBWPGiEmCLLR9kRGidgLY7Ac1rpSWwfGzTJpTmCU"
@ -59,4 +61,33 @@ async def check_token(security: HTTPAuthorizationCredentials = Depends(bearer)):
username: str = payload.get("sub") username: str = payload.get("sub")
return await get_user({"username": username}) return await get_user({"username": username})
except JWTError: 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="无权访问")

View File

@ -1,3 +1,5 @@
from tortoise import connections
from models import MenuModel from models import MenuModel
from schemas.menu import MenuIn from schemas.menu import MenuIn
@ -41,3 +43,27 @@ async def get_menu(kwargs):
async def del_menu(mid: int): async def del_menu(mid: int):
"""删除用户""" """删除用户"""
return await MenuModel.filter(id=mid).update(status=9) 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],
)

View File

@ -11,7 +11,7 @@ async def get_role_menus(rid: int):
db = connections.get("default") db = connections.get("default")
return await db.execute_query_dict( 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 FROM sys_menu as m, sys_role_menu WHERE m.id = sys_role_menu.mid
AND sys_role_menu.rid = (?) AND m.`status` = 1""", AND sys_role_menu.rid = (?) AND m.`status` = 1""",
[rid], [rid],

View File

@ -1,6 +1,7 @@
from fastapi import FastAPI from fastapi import FastAPI
from core.events import close_orm, init_orm from core.events import close_orm, init_orm
from core.exceptions import exception_handlers
from core.log import logger from core.log import logger
from core.middleware import middlewares from core.middleware import middlewares
from router.url import routes from router.url import routes
@ -10,14 +11,13 @@ app = FastAPI(
on_shutdown=[close_orm], on_shutdown=[close_orm],
routes=routes, routes=routes,
middleware=middlewares, middleware=middlewares,
exception_handlers=exception_handlers,
) )
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
for i in app.routes: for i in app.routes:
logger.info( logger.info(f"{i.path}, {i.methods}, {i.__dict__.get('summary')}, {i.endpoint}")
f"{i.path}, {i.methods}, {i.path_regex}, {i.__dict__.get('summary')}, {i.endpoint}"
)
uvicorn.run("main:app", reload=True) uvicorn.run("main:app", reload=True)

Binary file not shown.

Binary file not shown.

View File

@ -15,7 +15,6 @@ class MenuModel(Table):
identifier = fields.CharField(max_length=30, description="权限标识 user:add", null=True) identifier = fields.CharField(max_length=30, description="权限标识 user:add", null=True)
api = fields.CharField(max_length=128, description="接口地址", null=True) api = fields.CharField(max_length=128, description="接口地址", null=True)
method = fields.CharField(max_length=10, description="接口请求方式", null=True) method = fields.CharField(max_length=10, description="接口请求方式", null=True)
regx = fields.CharField(max_length=50, description="接口地址正则表达式", null=True)
class Meta: class Meta:
table = "sys_menu" table = "sys_menu"

View File

@ -4,17 +4,11 @@ from fastapi import Depends, routing
from controller.common import about, login from controller.common import about, login
from controller.menu import menu_add, menu_arr, menu_del from controller.menu import menu_add, menu_arr, menu_del
from controller.role import ( from controller.role import (assigned_menu, role_add, role_arr, role_del,
assigned_menu, role_has_menu, role_put, role_query)
role_add, from controller.user import (user_add, user_arr, user_del, user_info,
role_arr, user_list, user_put)
role_del, from core.security import check_permissions
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
class Route(routing.APIRoute): class Route(routing.APIRoute):
@ -113,48 +107,55 @@ class Route(routing.APIRoute):
) )
has_perm = {"dependencies": [Depends(check_permissions)]}
routes = [ routes = [
Route.post("/login", endpoint=login, tags=["公共"], summary="登录"), 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.get("/user", endpoint=user_arr, tags=["用户管理"], summary="用户列表", **has_perm),
Route.post("/user", endpoint=user_add, tags=["用户管理"], summary="用户新增"), Route.post("/user", endpoint=user_add, tags=["用户管理"], summary="用户新增", **has_perm),
Route.delete( Route.delete(
"/user/{pk}", "/user/{pk}", endpoint=user_del, tags=["用户管理"], summary="用户删除", **has_perm
endpoint=user_del,
tags=["用户管理"],
summary="用户删除",
), ),
Route.put("/user/{pk}", endpoint=user_put, tags=["用户管理"], summary="用户更新"), Route.put(
Route.get("/user/{pk}", endpoint=user_info, tags=["用户管理"], summary="用户信息"), "/user/{pk}", endpoint=user_put, tags=["用户管理"], summary="用户更新", **has_perm
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.get( 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( 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.get("/menu", endpoint=menu_arr, tags=["菜单管理"], summary="菜单列表", **has_perm),
Route.post("/menu", endpoint=menu_add, tags=["菜单管理"], summary="菜单新增"), Route.post("/menu", endpoint=menu_add, tags=["菜单管理"], summary="菜单新增", **has_perm),
Route.delete( Route.delete(
"/menu/{pk}", "/menu/{pk}", endpoint=menu_del, tags=["菜单管理"], summary="菜单删除", **has_perm
endpoint=menu_del,
tags=["菜单管理"],
summary="菜单删除",
dependencies=[Depends(check_token)],
), ),
] ]

View File

@ -15,7 +15,6 @@ class MenuBasic(BaseModel):
identifier: Optional[str] = Field(default=None, description="权限标识符 -> 按钮显示") identifier: Optional[str] = Field(default=None, description="权限标识符 -> 按钮显示")
api: Optional[str] = Field(default=None, description="后端接口地址") api: Optional[str] = Field(default=None, description="后端接口地址")
method: Optional[str] = Field(default=None, description="接口请求方法") method: Optional[str] = Field(default=None, description="接口请求方法")
regx: Optional[str] = Field(default=None, description="正则匹配")
class MenuIn(MenuBasic): class MenuIn(MenuBasic):

View File

@ -51,7 +51,6 @@ params = [
identifier=None, identifier=None,
api=None, api=None,
method=None, method=None,
regx=None,
).dict(), ).dict(),
), ),
( (
@ -66,7 +65,6 @@ params = [
identifier=None, identifier=None,
api=None, api=None,
method=None, method=None,
regx=None,
).dict(), ).dict(),
), ),
# 组件 # 组件
@ -81,8 +79,7 @@ params = [
pid=1, pid=1,
identifier=None, identifier=None,
api="/user", api="/user",
method="{'GET'}", method="GET",
regx="^/user$",
).dict(), ).dict(),
), ),
( (
@ -96,8 +93,7 @@ params = [
pid=1, pid=1,
identifier=None, identifier=None,
api="/role", api="/role",
method="{'GET'}", method="GET",
regx="^/role$",
).dict(), ).dict(),
), ),
( (
@ -111,8 +107,7 @@ params = [
pid=1, pid=1,
identifier=None, identifier=None,
api="/menu", api="/menu",
method="{'GET'}", method="GET",
regx="^/menu$",
).dict(), ).dict(),
), ),
( (
@ -126,8 +121,7 @@ params = [
pid=2, pid=2,
identifier=None, identifier=None,
api="/about", api="/about",
method="{'GET'}", method="GET",
regx="^/about",
).dict(), ).dict(),
), ),
# 按钮 # 按钮
@ -142,8 +136,7 @@ params = [
pid=3, pid=3,
identifier="user:create", identifier="user:create",
api="/user", api="/user",
method="{'POST'}", method="POST",
regx="^/user$",
).dict(), ).dict(),
), ),
( (
@ -157,8 +150,7 @@ params = [
pid=3, pid=3,
identifier="user:delete", identifier="user:delete",
api="/user/{pk}", api="/user/{pk}",
method="{'DELETE'}", method="DELETE",
regx="^/user/(?P<pk>[^/]+)$",
).dict(), ).dict(),
), ),
( (
@ -172,8 +164,7 @@ params = [
pid=3, pid=3,
identifier="user:update", identifier="user:update",
api="/user/{pk}", api="/user/{pk}",
method="{'PUT'}", method="PUT",
regx="^/user/(?P<pk>[^/]+)$",
).dict(), ).dict(),
), ),
( (
@ -187,8 +178,7 @@ params = [
pid=3, pid=3,
identifier="user:get", identifier="user:get",
api="/user/{pk}", api="/user/{pk}",
method="{'GET'}", method="GET",
regx="^/user/(?P<pk>[^/]+)$",
).dict(), ).dict(),
), ),
( (
@ -202,8 +192,7 @@ params = [
pid=3, pid=3,
identifier="user:query", identifier="user:query",
api="/user/query", api="/user/query",
method="{'POST'}", method="POST",
regx="^/user/query$",
).dict(), ).dict(),
), ),
# 角色管理 # 角色管理
@ -218,8 +207,7 @@ params = [
pid=4, pid=4,
identifier="role:create", identifier="role:create",
api="/role", api="/role",
method="{'POST'}", method="POST",
regx="^/role$",
).dict(), ).dict(),
), ),
( (
@ -233,8 +221,7 @@ params = [
pid=4, pid=4,
identifier="role:delete", identifier="role:delete",
api="/role/{pk}", api="/role/{pk}",
method="{'DELETE'}", method="DELETE",
regx="^/role/(?P<pk>[^/]+)$",
).dict(), ).dict(),
), ),
( (
@ -248,8 +235,7 @@ params = [
pid=4, pid=4,
identifier=None, identifier=None,
api="/role/{rid}/menu", api="/role/{rid}/menu",
method="{'GET'}", method="GET",
regx="^/role/(?P<rid>[^/]+)/menu$",
).dict(), ).dict(),
), ),
( (
@ -263,8 +249,7 @@ params = [
pid=4, pid=4,
identifier="", identifier="",
api="/role/query", api="/role/query",
method="{'POST'}", method="POST",
regx="^/role/query$",
).dict(), ).dict(),
), ),
( (
@ -278,8 +263,7 @@ params = [
pid=4, pid=4,
identifier="role:assign", identifier="role:assign",
api="/role/assigned/menu", api="/role/assigned/menu",
method="{'POST'}", method="POST",
regx="^/role/assigned/menu$",
).dict(), ).dict(),
), ),
( (
@ -293,8 +277,7 @@ params = [
pid=4, pid=4,
identifier="role:update", identifier="role:update",
api="/role", api="/role",
method="{'PUT'}", method="PUT",
regx="^/role$",
).dict(), ).dict(),
), ),
# 菜单管理的权限 # 菜单管理的权限
@ -309,8 +292,7 @@ params = [
pid=5, pid=5,
identifier="menu:create", identifier="menu:create",
api="/menu", api="/menu",
method="{'POST'}", method="POST",
regx="^/menu$",
).dict(), ).dict(),
), ),
( (
@ -324,8 +306,7 @@ params = [
pid=5, pid=5,
identifier="menu:delete", identifier="menu:delete",
api="/menu/{pk}", api="/menu/{pk}",
method="{'DELETE'}", method="DELETE",
regx="/menu/(?P<pk>[^/]+)$",
).dict(), ).dict(),
), ),
# 分配权限 # 分配权限
@ -333,7 +314,7 @@ params = [
"/role/assigned/menu", "/role/assigned/menu",
RoleMenuIn(rid=1, menus=[num for num in range(1, 20)]).dict(), 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()),
] ]

View File

@ -12,6 +12,14 @@ export const userStore = defineStore('user', () => {
// getter // getter
const accessToken = computed(() => 'Bearer ' + token.value) 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 // 非setup语法时的actions
const loginAction = async (data) => { const loginAction = async (data) => {
@ -34,7 +42,8 @@ export const userStore = defineStore('user', () => {
ElMessage.success("登录成功.") ElMessage.success("登录成功.")
} }
return { token, accessToken, userInfo, userMenus, loginAction } return { token, accessToken, userInfo, userMenus,
$reset, loginAction }
}, { }, {
persist: true, // 解决pinia刷新时数据丢失问题 persist: true, // 解决pinia刷新时数据丢失问题
}) })

View File

@ -1,5 +1,12 @@
<script setup> <script setup>
import router from '@/router';
import { userStore } from '@/stores/user';
const store = userStore()
const logout = () => {
store.$reset()
router.push('/login')
}
</script> </script>
<template> <template>
@ -7,7 +14,9 @@
<el-container> <el-container>
<el-aside width="200px">Aside</el-aside> <el-aside width="200px">Aside</el-aside>
<el-container> <el-container>
<el-header>Header</el-header> <el-header>Header <el-button @click="logout">
注销
</el-button></el-header>
<el-main>Main</el-main> <el-main>Main</el-main>
<el-footer>Footer</el-footer> <el-footer>Footer</el-footer>
</el-container> </el-container>