Flask學習「一」(按鈕,角色,菜單,使用者,權限)
很榮幸有時間能靜下心來寫在這篇文章,前段時間寫了一些沒有營養的文章對那些關注我的同學來說非常抱歉,接下來的一段日子裡會圍繞近期所做的Flask項目寫一系列的部落格,以記錄自己的不足。
鑒于可能有些小白可能會看到這篇文章,于是我盡量寫的通俗易懂。
接下來進入正題,我這篇文章要寫的是一個系統的權限部分。權限的控制對于一個優秀的系統來說至關重要,但是對于權限的設計和把空是比較麻煩的。
一般如果我們不考慮按鈕的話,邏輯大緻如下:
把菜單和權限、權限使用者關聯起來。
1、使用者頁面,可以增删改查,并且還要有一個配置設定權限的按鈕。
2、權限頁面,可以增删改查,并且有一個配置設定使用者的按鈕和一個配置設定菜單的按鈕。
3、建立兩個表,分别為使用者權限表(儲存使用者ID和權限ID)、權限菜單表(儲存權限ID和菜單ID)。
4、當在使用者頁面中選中一個使用者,點選使用者的“配置設定權限”按鈕時,打開展示所有權限的頁面(并把使用者ID傳進去),左邊展示所有還沒有配置設定的權限清單,右邊展現已經配置設定的權限清單,然後選擇需要配置設定的左邊權限後,點選配置設定,把資料配置設定到右邊已配置設定的清單中,然後點選“确定”按鈕,把使用者ID和選擇的權限ID儲存到使用者權限表。
5、當在權限頁面選中一個權限,并點選“配置設定使用者”時,處理方式和4相同,當選擇需要配置設定權限的使用者後,同樣把使用者ID和權限ID儲存到使用者權限表。
6、當在權限頁面選中一個權限,并點選“配置設定菜單”時,打開一個樹展現所有菜單的頁面,每個樹節點前面有一個複選框,并把這個權限已經配置設定的樹預設選中,然後在要配置設定的菜單節點樹前面的複選框上選中,最後儲存資料,把權限Id和所有選中的菜單ID儲存到權限菜單表。
7、當使用者登陸系統的時候,首先檢查使用者輸入的密碼資訊,如果密碼正确,再根據使用者倒查使用者權限表,再通過使用者權限表查到的權限,到權限菜單表查詢相應的菜單,再把相應的菜單展示出來。
上面便是不考慮按鈕的情況下的業務邏輯,其實加上按鈕的話也是差不多的,因為按鈕隸屬于菜單,隻有給某個使用者配置設定了某個角色,這個使用者才能在登入的時候看到他所擁有角色對應下的菜單和按鈕,這樣即完成了角色的權限控制。
接下來開始我們的項目。
首先根據上面的業務描述,我們大概可以用到的表和字段如下:
user表(id,name,tel,email,password) # 使用者表
role表(id,name,description) # 角色表
user_role表(id,user_id,role_id) # 使用者角色表
menu表(id,parent_id,lay,name,code,description) # 菜單表
action表(id,menu_id,name,code,description) # 按鈕表
role_menu(id,role_id,menu_id) # 角色菜單表
role_action(id,role_id,action_id) # 角色按鈕表
emmm,這幾張表的關系大概如上吧。
大概邏輯有了,現在開始寫代碼:
#----------------------------------start-----------------------------------#
'''
我們的架構使用Flask+sqlalchemy+flask_restplus
sqlalchemy為ORM資料庫映射 PS:sqlalchemy真的非常強大 使用起來非常友善
flask_restplus是swagger所呈現出來的一種網頁端接口測試工具 最大的有點是可以避免寫接口文檔
'''
# 根據user_id查詢 required=True為必填項
page_parser.add_argument('user_id', type=int, required=True, location='args')
# 使用者角色post新增/修改傳入參數
user_role_model = api.model('RoleUserRole', {
'role_id_list': fields.String('role id list 以逗号隔開","'),
'user_id': fields.Integer
})
# flask_restplus頁面展示url /flask路由注冊/需注冊到藍圖上
@api.route('/role_by_user')
# flask_restplus定義每一個類名展現在swagger的NameSpace上
class RoleByUser(Resource):
@api.expect(page_parser)
‘’‘
查詢已經配置設定過角色的使用者 以使用者為主體
’‘’
def get(self):
# 自定義驗證傳入參數是否合法
form = RoleByUserForm().validate_for_api()
#實作代碼子產品化 将可複用查詢條件拆分出來 放在最後定義成了一個單獨的方法
task_filter = _form_and_task(form)
user_id = form.user_id.data
# 增加查詢條件
task_filter.append(UserRole.user_id == user_id)
page = Role.query.outerjoin(UserRole, Role.id == UserRole.role_id) \
# *task_filter為可變參數,可以傳入元組/清單
.filter(*task_filter).order_by(
# sqlalchemy根據建立時間排序
text('role.create_time desc')
# paginate()分頁對象 傳入定義号的頁數
).paginate()
return Role().page(page)
@api.route('/role_by_not_user')
class RoleByNotUser(Resource):
# 和上面的類似 查詢未配置設定角色的使用者
@api.expect(page_parser)
def get(self):
form = RoleByUserForm().validate_for_api()
task_filter = _form_and_task(form)
user_id = form.user_id.data
# UserRole.user_id != user_id 查詢未配置設定角色的使用者
task_filter.append(UserRole.user_id != user_id)
page = Role.query.outerjoin(UserRole, Role.id == UserRole.role_id) \
.filter(*task_filter).order_by(
text('role.create_time desc')
).paginate()
return Role().page(page)
# 新增 一個使用者可能對應多個角色 傳入role_id_list
@api.expect(user_role_model)
def post(self):
form = RoleUserPostForm().validate_for_api()
user_id, role_id_list = form.user_id.data, form.role_id_list.data
# 傳入role_id_list使用“,”分開 使用split從每個“,”處分開
if role_id_list:
user_role_list = []
role_id_list = role_id_list.split(',')
# 周遊role_id_list 将每個role_id存入上面定義的user_role_list清單中
# 調用我們自定義的save_all方法 将每個role_id存入UserRole表
for role_id in role_id_list:
user_role = UserRole()
user_role.role_id = role_id
user_role.user_id = int(user_id)
user_role_list.append(user_role)
UserRole().save_all(user_role_list)
’‘’
權限設定
’‘’
role_action_menu_parser = reqparse.RequestParser()
role_action_menu_parser.add_argument('role_id', type=int, required=True, location='args')
menu_action_lists = api.model('RoleActionMenuList', {
'mid': fields.Integer,
'type': fields.Integer
})
# 接收的參數為menu_action_list和role_id,menu_action_list中存的是mid和type
# 這裡的type是為了區分菜單和按鈕 0-菜單 1-按鈕
role_action_menu_model = api.model('RoleActionMenu', {
'menu_action_list': fields.List(fields.Nested(menu_action_lists)),
'role_id': fields.Integer
})
‘’‘
namedtuple(命名元組)是繼承自tuple的子類 namedtuple建立一個和tuple類似的對象 而且對象擁有可通路的屬性
普通tuple類型的成員 隻能通過索引通路 namedtuple在此基礎上還提供了通過名稱通路的方式
’‘’
# 我們使用一個命名元組來定義按鈕和菜單的樹形集合
menu_action_tree = namedtuple('MenuActionTree', ['id', 'name', 'parent_id', 'lay', 'is_select', 'has_child', 'type'])
@api.route('/role_action_menu')
class RoleActionMenu(Resource):
@api.expect(role_action_menu_parser)
‘’‘
查詢該角色所能查到的所有的菜單和按鈕
’‘’
def get(self):
form = RoleIdForm().validate_for_api()
role_id = form.role_id.data
menus = Menu.query.filter().all() # 菜單
actions = Action.query.filter().all() # 按鈕
# 通過自定義樹形菜單和按鈕清單,通過role_id查詢拼接目前角色所能看到的菜單和按鈕
# 分别構造拼接菜單和按鈕樹形集合 并将菜單和按鈕的樹形合并
menu_action_trees = _menu_tree(role_id, menus)
menu_action_trees += _action_tree(role_id, actions, menus)
# 通過自定義get_tree方法将最後合并好的資料集合轉化為json傳給前台
tree = get_tree(menu_action_trees)
return tree
‘’‘
新增角色菜單和按鈕
’‘’
@api.expect(role_action_menu_model)
def post(self):
form = RoleMenuActionForm().validate_for_api()
role_id, menu_action_list = form.role_id.data, form.menu_action_list.data
# 過濾 區分菜單和按鈕
if menu_action_list:
role_action = list(filter(lambda x: x['type'] == 1, menu_action_list))
role_menu = list(filter(lambda x: x['type'] == 0, menu_action_list))
# 使用自定義方法分别儲存菜單和按鈕到role_menu和role_action表
with db.auto_commit():
_save_menu(role_id, role_menu)
_save_action(role_id, role_action)
# 存儲菜單
def _save_menu(role_id, role_menu):
# 每次在存之前我們先删除該角色之前存儲過的菜單
RoleMenu.query.filter_by(role_id=role_id).delete()
new_role_menu = []
# 周遊role_menu表 通過role_id将給該角色添加菜單
for m in role_menu:
role_menu = RoleMenu()
role_menu.role_id = role_id
role_menu.menu_id = m['mid']
new_role_menu.append(role_menu)
RoleMenu().save_all(new_role_menu)
#存儲按鈕
def _save_action(role_id, role_action):
# 和菜單一樣 在新增之前我們要删除之前存儲過的按鈕
RoleAction.query.filter_by(role_id=role_id).delete()
new_role_action = []
# 周遊role_acton表通過role_id将給該角色添加角色
for m in role_action:
role_action = RoleAction()
role_action.role_id = role_id
role_action.action_id = m['mid']
new_role_action.append(role_action)
RoleAction().save_all(new_role_action)
# 拼接action樹形
def _action_tree(role_id, actions, menus):
menu_action_trees = []
role_actions = RoleAction.query.filter(UserRole.role_id == role_id).all()
for action in actions:
action_parent = list(filter(lambda x: x.id == action.menu_id, menus))
# 判斷層級
if len(action_parent) > 0:
lay = action_parent[0].lay + 1
else:
lay = 0
# 是否選中 1-選中 0-未選中
is_select = [False for role_action in role_actions if role_action.action_id == action.id]
if is_select:
is_select = 1
else:
is_select = 0
# 按照前面定義命名元組參數順序按照順序傳入對應參數
mct = menu_action_tree(
str(action.id) + '$action', # 為了區分按鈕和菜單id使用$action分割
action.name,
action.menu_id,
lay,
is_select,
0, # 按鈕是最後一級
1 # 0-菜單 1-按鈕
)
menu_action_trees.append(mct)
return menu_action_trees
# 拼接菜單樹形
def _menu_tree(role_id, menus):
menu_action_trees = []
role_menus = RoleMenu.query.filter(UserRole.role_id == role_id).all()
for menu in menus:
# 通過清單推導式判斷有無選中
is_select = [False for role_menu in role_menus if role_menu.menu_id == menu.id]
if is_select:
is_select = 1
else:
is_select = 0
mct = menu_action_tree(
menu.id,
menu.name,
menu.parent_id,
menu.lay,
is_select,
0, # 不好判斷暫定為0
0 # 0-菜單 1-按鈕
)
menu_action_trees.append(mct)
return menu_action_trees
# 通過姓名模糊查詢
def _form_and_task(form):
name = form.name.data
task_filter = [
Role.name.like('%' + name + '%') if name is not None else text(''),
]
return task_filter
#----------------------------------end------------------------------------#
這樣我們就完成了按鈕,角色,菜單,使用者,權限的校驗,文中少數自定義類或方法由于寫在了基類中,等到後面會慢慢列出。另外文章前面是以使用者為主體的角色綁定使用者,在使用者頁面還應該有以角色為主體的使用者綁定角色,但是兩者都不盡相同,是以在本文中暫不列出,後續如果有需要的話再補上!