前面的菜单, 部门, 职位与管理员管理功能完成后, 接下来要处理的是将它们关联起来, 根据职位管理中选定的权限控制菜单显示以及页面数据的访问和操作.
那么要怎么改造呢? 我们可以通过用户的操作步骤来一步步进行处理, 具体思路如下:
1. 用户在管理端登录时, 通过用户记录所绑定的职位信息, 来确定用户所拥有的权限. 我们可以在登录接口中, 将该管理员的职位 id 存储到 session 中, 以方便后续的调用.
2. 登录成功后, 跳转进入管理界口, 在获取菜单列表时, 需要对菜单列表进行处理, 只列出当前用户有权限的菜单项.
3. 在点击菜单进入相关数据页面或在数据页面进行增删改查等操作时, 需要进行权限判断, 判断是否有权限进行查看或操作. 由于我们是前后端分离, 所以权限只需要在接口进行处理.
首先我们来简单改造一下登录接口 login.py, 只需要在将职位 id 存储到 session 中就可以了
- ##############################################################
- ### 把用户信息保存到 session 中 ###
- ##############################################################
- manager_id = manager_result.get('id', 0)
- s['id'] = manager_id
- s['login_name'] = username
- s['positions_id'] = manager_result.get('positions_id', '')
- s.save()
找到上面内容, 在里面插入 s['positions_id'] = manager_result.get('positions_id', '')
接下来改造菜单列表接口 menu_info.py 文件的 @get('/api/main/menu_info/')接口, 我们需要做以下操作:
1. 首先从 session 中获取当前用户的职位 id, 然后根据职位 id 从职位表中读取对应的权限数据
2. 其次在菜单的遍历组装过程中, 添加判断用户的权限, 没有权限的菜单项直接过滤掉
- @get('/api/main/menu_info/')
- def callback():
- """
- 主页面获取菜单列表数据
- """
- # 获取当前用户权限
- session = web_helper.get_session()
- if session:
- _positions_logic = positions_logic.PositionsLogic()
- page_power = _positions_logic.get_page_power(session.get('positions_id'))
- else:
- page_power = ''
- if not page_power:
- return web_helper.return_msg(-404, '您的登录已超时, 请重新登录')
- _menu_info_logic = menu_info_logic.MenuInfoLogic()
- # 读取记录
- result = _menu_info_logic.get_list('*', 'is_show and is_enabled', orderby='sort')
- if result:
- # 定义最终输出的 html 存储变量
- html = '' for model in result.get('rows'):
- # 检查是否有权限
- if ',' + str(model.get('id')) + ',' in page_power:
- # 提取出第一级菜单
- if model.get('parent_id') == 0:
- # 添加一级菜单
- temp = """ <dl id="menu-%(id)s">
- <dt><i class="Hui-iconfont">%(icon)s</i> %(name)s<i class="Hui-iconfont menu_dropdown-arrow"></i></dt>
- <dd>
- <ul>
- """% {'id': model.get('id'),'icon': model.get('icon'),'name': model.get('name')}
- html = html + temp
- # 从所有菜单记录中提取当前一级菜单下的子菜单
- for sub_model in result.get('rows'):
- # 检查是否有权限
- if ',' + str(sub_model.get('id')) + ',' in page_power:
- # 如果父 id 等于当前一级菜单 id, 则为当前菜单的子菜单
- if sub_model.get('parent_id') == model.get('id'):
- temp = """ <li><a data-href="%(page_url)s"data-title="%(name)s"href="javascript:void(0)">%(name)s</a></li>
- """% {'page_url': sub_model.get('page_url'),'name': sub_model.get('name')}
- html = html + temp
- # 闭合菜单 html
- temp = """
- </ul>
- </dd>
- </dl>
- """
- html = html + temp
- return web_helper.return_msg(0, '成功', {'menu_html': html})
- else:
- return web_helper.return_msg(-1, "查询失败")
- 第 9 与第 10 行, 就是从职位表中, 读取指定职位 id 的权限 page_power 字段值, 第 24 行与第 39 行中, 只需要判断当前菜单 id 是否存在 page_power 字段值中, 就可以判断是否拥有该菜单权限了, 因为在前面职位管理那里, 勾选了指定菜单 id 后, 就会将菜单的 id 存储到这个字段中.
- 由于可能多处需要读取权限 page_power 字段值, 这里我们需要在职位逻辑类 positions_logic.py 中添加 get_page_power()方法, 来获取其值出来使用.
- def get_page_power(self, positions_id):
- """获取当前用户权限"""
- page_power = self.get_value_for_cache(positions_id, 'page_power')
- if page_power:
- return ',' + page_power + ','
- else:
- return ','
- 我们调用 ORM 的 get_value_for_cache()方法, 直接通过主键 id 来读取我们想要的字段值, 并在权限字串两端添加逗号, 因为我们在比较菜单 id 是否存在于权限字串时, 不加上逗号可能会出错, 比如说权限串有 2,10,11, 如果我们直接比较 1 是否存在于权限串中, 如果不转为 list, 直接字符串比较, 返回结果就会为 True, 因为 10 和 11 都存在 1, 而各增加逗号以后比较就不一样了,,2,10,11, 与, 1, 比较肯定返回的是 False, 也就是说当前管理员没有拥有 1 这个菜单 id 的权限.
- PS: 完成菜单列表功能的改造后, 记得检查菜单列表页面 (main.html) 和改造的接口是否在上一章节结束后, 添加到菜单管理项中, 并在职位管理中将对应的权限项打上勾, 如果没有的话, 完成本文改造, 登录后台将会提示你没有访问权限.
- 最后要处理的是后台管理各接口的权限判断, 由于 bottle 勾子 (@hook('before_request')) 直接获取当前访问的路由(接口), 所获取到的都有具体值(比如:@get('/system/menu_info/<id:int>/') 这个路由, 在勾子中取到的是 / system/menu_info/1/, 由于 id 值是不固定的, 我们要处理起来会很麻), 所以我们只能在每个接口中直接处理, 也就是说我们需要在每个接口中, 添加固定的权限判断方法调用.
- 而权限的处理需要对数据库对数据库进行读取操作, 所以我们可以在逻辑层文件夹中 (logic) 添加一个通用的逻辑层模块_common_logic.py, 将权限判断方法在这个文件中实现, 方便调用.
- 这里的权限判断实现原理是: 通过获取 web 来路 html 页面名称, 当前接口访问方式(method), 当前访问的接口路由名称, 将它们组成一个 key 值, 从菜单权限初始化缓存中读取出对应的菜单实体(后面会讲到如何生成这个菜单权限缓存), 提取当前所访问接口所对应的菜单 id 值, 然后通过从 session 中获取当前用户的职位 id, 获取当前用户所拥有的职位权限, 将菜单 id 与职位权限进行比较, 判断用户是否拥有当前所访问的接口权限, 从而达到对权限的访问控制.
- 具体实现这个权限判断方法, 有以下步骤:
- 1. 首先我们需要获取 web 的来路地址 HTTP_REFERER, 由于我们在前面菜单管理中, 录入的 html 页面地址不包括域名和参数, 所以来路地址需要去掉当前域名和? 号后面的附加参数, 只保留 html 页面名称.
- 2. 直接从从 bottle 的 request 中, 读取当前访问接口的路由值(rule)
- 3. 从 bottle 的 request 中获取当前访问接口的方式(get/post/put/delete)
- 4. 将前面三步获取的值组合成菜单对应的唯一 key, 然后在菜单权限缓存中读取对应的菜单实体
- 5. 如果菜单记录实体不存在, 则表达当前接口未注册或注册时所提交的信息错误, 当前用户没有该接口的访问权限
- 6. 从 session 中获取当前用户登录时所存储的职位 id, 然后通过该 id 读取对应的职位权限
- 7. 从菜单实体中提取菜单 id, 与职位权限进行比较, 判断当前用户是否拥有访问该接口的权限, 如果有则跳过, 没有则拒绝访问.
- 具体代码如下:
- #!/usr/bin/env python
- # coding=utf-8
- from bottle import request
- from common import web_helper
- from logic import menu_info_logic, positions_logic
- def check_user_power():
- """检查当前用户是否有访问当前接口的权限"""
- # 获取当前页面原始路由
- rule = request.route.rule
- # 获取当前访问接口方式(get/post/put/delete)
- method = request.method.lower()
- # 获取来路 url
- http_referer = request.environ.get('HTTP_REFERER')
- if http_referer:
- # 提取页面 url 地址
- index = http_referer.find('?')
- if index == -1:
- url = http_referer[http_referer.find('/', 8) + 1:]
- else:
- url = http_referer[http_referer.find('/', 8) + 1: index]
- else:
- url = ''
- # 组合当前接口访问的缓存 key 值
- key = url + method + '(' + rule + ')'
- # 从菜单权限缓存中读取对应的菜单实体
- menu_info = menu_info_logic.MenuInfoLogic()
- model = menu_info.get_model_for_url(key)
- if not model:
- web_helper.return_raise(web_helper.return_msg(-1, "您没有访问权限 1" + key))
- # 读取 session
- session = web_helper.get_session()
- if session:
- # 从 session 中获取当前用户登录时所存储的职位 id
- positions = positions_logic.PositionsLogic()
- page_power = positions.get_page_power(session.get('positions_id'))
- # 从菜单实体中提取菜单 id, 与职位权限进行比较, 判断当前用户是否拥有访问该接口的权限
- if page_power.find(',' + str(model.get('id', -1)) + ',') == -1:
- web_helper.return_raise(web_helper.return_msg(-1, "您没有访问权限 2"))
- else:
- web_helper.return_raise(web_helper.return_msg(-404, "您的登录已失效, 请重新登录"))
- 对于前面所讲的菜单权限缓存, 下面详细讲解一下.
- 由于菜单跟接口都很多, 我们在做权限判断时, 就需要在访问接口时, 自动匹配找到该接口对应的菜单项, 然后才可以根据菜单 id 和权限字符进行比较, 判断是否拥有操作权限, 而自动匹配这里如果直接通过数据库查找的话, 操作会比较复杂, 也会影响使用性能, 所以我们可以通过将在菜单管理中注册的菜单项进行分解, 按一定的规则组合生成对应的缓存 key, 存储到 nosql 中, 当访问接口时, 我们根据规则组合成对应的 key 直接在 nosql 中查找就可以实现我们想要的功能了. 当然第一次访问或我们清除缓存后, 这些 key 值是不存在的, 所以我们可以加个判断, 如果缓存不存在时, 重新加载生成对应的 key 就可以了.
- 具体代码如下:
- def get_model_for_url(self, key):
- """通过当前页面路由 url, 获取菜单对应的记录"""
- # 使用 md5 生成对应的缓存 key 值
- key_md5 = encrypt_helper.md5(key)
- # 从缓存中提取菜单记录
- model = cache_helper.get(key_md5)
- # 记录不存在时, 运行记录载入缓存程序
- if not model:
- self._load_cache()
- model = cache_helper.get(key_md5)
- return model
- def _load_cache(self):
- """全表记录载入缓存"""
- # 生成缓存载入状态 key, 主要用于检查是否已执行了菜单表载入缓存判断
- cache_key = self.__table_name + '_is_load'
- # 将自定义的 key 存储到全局缓存队列中(关于全局缓存队列请查看前面 ORM 对应章节说明)
- self.add_relevance_cache_in_list(cache_key)
- # 获取缓存载入状态, 检查记录是否已载入缓存, 是的话则不再执行
- if cache_helper.get(cache_key):
- return
- # 从数据库中读取全部记录
- result = self.get_list()
- # 标记记录已载入缓存
- cache_helper.set(cache_key, True)
- # 如果菜单表没有记录, 则直接退出
- if not result:
- return
- # 循环遍历所有记录, 组合处理后, 存储到 nosql 缓存中
- for model in result.get('rows', {}):
- # 提取菜单页面对应的接口(后台菜单管理中的接口值, 同一个菜单操作时, 经常需要访问多个接口, 所以这个值有中存储多们接口值)
- interface_url = model.get('interface_url', '')
- if not interface_url:
- continue
- # 获取前端 html 页面地址
- page_url = model.get('page_url', '')
- # 同一页面接口可能有多个, 所以需要进行分割
- interface_url_arr = interface_url.replace('\n', '').replace(' ','').split(',')
- # 逐个接口处理
- for interface in interface_url_arr:
- # html + 接口组合生成 key
- url_md5 = encrypt_helper.md5(page_url + interface)
- # 存储到全局缓存队列中, 方便菜单记录更改时, 自动清除这些自定义缓存
- self.add_relevance_cache_in_list(url_md5)
- # 存储到 nosql 缓存
- cache_helper.set(url_md5, model)
- 这里的权限管理逻辑有点绕, 需要认真思考与 debug 检查, 才能真正掌握. 另外, 也可以通过后台菜单管理中, 故意修改菜单项的某些值, 来检查这里的代码处理与变化.
- 完成以上代码以后, 权限的处理就完成了, 接下来只需要在每个后台管理接口中添加下面代码就可以做到接口的访问权限控制了.
- @get('/api/main/menu_info/')
- def callback():
- """
- 主页面获取菜单列表数据
- """
- # 检查用户权限
- _common_logic.check_user_power()
具体大家可以查看文章后面提供的源码, 看看后台管理接口处理就清楚了.
本文对应的源码下载
来源: https://www.cnblogs.com/EmptyFS/p/9636355.html