我的第一个python web开发框架(39)——后台接口权限访问控制处理
前面的菜单、部门、职位与管理员管理功能完成后,接下来要处理的是将它们关联起来,根据职位管理中选定的权限控制菜单显示以及页面数据的访问和操作。
那么要怎么改造呢?我们可以通过用户的操作步骤来一步步进行处理,具体思路如下:
1.用户在管理端登录时,通过用户记录所绑定的职位信息,来确定用户所拥有的权限。我们可以在登录接口中,将该管理员的职位id存储到session中,以方便后续的调用。
2.登录成功后,跳转进入管理界口,在获取菜单列表时,需要对菜单列表进行处理,只列出当前用户有权限的菜单项。
3.在点击菜单进入相关数据页面或在数据页面进行增删改查等操作时,需要进行权限判断,判断是否有权限进行查看或操作。由于我们是前后端分离,所以权限只需要在接口进行处理。
首先我们来简单改造一下登录接口login.py,只需要在将职位id存储到session中就可以了
1 ############################################################## 2 ### 把用户信息保存到session中 ### 3 ############################################################## 4 manager_id = manager_result.get('id', 0) 5 s['id'] = manager_id 6 s['login_name'] = username 7 s['positions_id'] = manager_result.get('positions_id', '') 8 s.save()
找到上面内容,在里面插入 s[‘positions_id’] = manager_result.get(‘positions_id’, ”)
接下来改造菜单列表接口menu_info.py文件的@get(‘/api/main/menu_info/’)接口,我们需要做以下操作:
1.首先从session中获取当前用户的职位id,然后根据职位id从职位表中读取对应的权限数据
2.其次在菜单的遍历组装过程中,添加判断用户的权限,没有权限的菜单项直接过滤掉
1 @get('/api/main/menu_info/') 2 def callback(): 3 """ 4 主页面获取菜单列表数据 5 """ 6 # 获取当前用户权限 7 session = web_helper.get_session() 8 if session: 9 _positions_logic = positions_logic.PositionsLogic() 10 page_power = _positions_logic.get_page_power(session.get('positions_id')) 11 else: 12 page_power = '' 13 if not page_power: 14 return web_helper.return_msg(-404, '您的登录已超时,请重新登录') 15 16 _menu_info_logic = menu_info_logic.MenuInfoLogic() 17 # 读取记录 18 result = _menu_info_logic.get_list('*', 'is_show and is_enabled', orderby='sort') 19 if result: 20 # 定义最终输出的html存储变量 21 html = '' 22 for model in result.get('rows'): 23 # 检查是否有权限 24 if ',' + str(model.get('id')) + ',' in page_power: 25 # 提取出第一级菜单 26 if model.get('parent_id') == 0: 27 # 添加一级菜单 28 temp = """ 29 <dl id="menu-%(id)s"> 30 <dt><i class="Hui-iconfont">%(icon)s</i> %(name)s<i class="Hui-iconfont menu_dropdown-arrow"></i></dt> 31 <dd> 32 <ul> 33 """ % {'id': model.get('id'), 'icon': model.get('icon'), 'name': model.get('name')} 34 html = html + temp 35 36 # 从所有菜单记录中提取当前一级菜单下的子菜单 37 for sub_model in result.get('rows'): 38 # 检查是否有权限 39 if ',' + str(sub_model.get('id')) + ',' in page_power: 40 # 如果父id等于当前一级菜单id,则为当前菜单的子菜单 41 if sub_model.get('parent_id') == model.get('id'): 42 temp = """ 43 <li><a data-href="%(page_url)s" data-title="%(name)s" href="javascript:void(0)">%(name)s</a></li> 44 """ % {'page_url': sub_model.get('page_url'), 'name': sub_model.get('name')} 45 html = html + temp 46 47 # 闭合菜单html 48 temp = """ 49 </ul> 50 </dd> 51 </dl> 52 """ 53 html = html + temp 54 55 return web_helper.return_msg(0, '成功', {'menu_html': html}) 56 else: 57 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()方法,来获取其值出来使用。
1 def get_page_power(self, positions_id): 2 """获取当前用户权限""" 3 page_power = self.get_value_for_cache(positions_id, 'page_power') 4 if page_power: 5 return ',' + page_power + ',' 6 else: 7 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,与职位权限进行比较,判断当前用户是否拥有访问该接口的权限,如果有则跳过,没有则拒绝访问。
具体代码如下:
1 #!/usr/bin/env python 2 # coding=utf-8 3 4 from bottle import request 5 from common import web_helper 6 from logic import menu_info_logic, positions_logic 7 8 def check_user_power(): 9 """检查当前用户是否有访问当前接口的权限""" 10 # 获取当前页面原始路由 11 rule = request.route.rule 12 # 获取当前访问接口方式(get/post/put/delete) 13 method = request.method.lower() 14 15 # 获取来路url 16 http_referer = request.environ.get('HTTP_REFERER') 17 if http_referer: 18 # 提取页面url地址 19 index = http_referer.find('?') 20 if index == -1: 21 url = http_referer[http_referer.find('/', 8) + 1:] 22 else: 23 url = http_referer[http_referer.find('/', 8) + 1: index] 24 else: 25 url = '' 26 27 # 组合当前接口访问的缓存key值 28 key = url + method + '(' + rule + ')' 29 # 从菜单权限缓存中读取对应的菜单实体 30 menu_info = menu_info_logic.MenuInfoLogic() 31 model = menu_info.get_model_for_url(key) 32 if not model: 33 web_helper.return_raise(web_helper.return_msg(-1, "您没有访问权限1" + key)) 34 35 # 读取session 36 session = web_helper.get_session() 37 if session: 38 # 从session中获取当前用户登录时所存储的职位id 39 positions = positions_logic.PositionsLogic() 40 page_power = positions.get_page_power(session.get('positions_id')) 41 # 从菜单实体中提取菜单id,与职位权限进行比较,判断当前用户是否拥有访问该接口的权限 42 if page_power.find(',' + str(model.get('id', -1)) + ',') == -1: 43 web_helper.return_raise(web_helper.return_msg(-1, "您没有访问权限2")) 44 else: 45 web_helper.return_raise(web_helper.return_msg(-404, "您的登录已失效,请重新登录"))
对于前面所讲的菜单权限缓存,下面详细讲解一下。
由于菜单跟接口都很多,我们在做权限判断时,就需要在访问接口时,自动匹配找到该接口对应的菜单项,然后才可以根据菜单id和权限字符进行比较,判断是否拥有操作权限,而自动匹配这里如果直接通过数据库查找的话,操作会比较复杂,也会影响使用性能,所以我们可以通过将在菜单管理中注册的菜单项进行分解,按一定的规则组合生成对应的缓存key,存储到nosql中,当访问接口时,我们根据规则组合成对应的key直接在nosql中查找就可以实现我们想要的功能了。当然第一次访问或我们清除缓存后,这些key值是不存在的,所以我们可以加个判断,如果缓存不存在时,重新加载生成对应的key就可以了。
具体代码如下:
1 def get_model_for_url(self, key): 2 """通过当前页面路由url,获取菜单对应的记录""" 3 # 使用md5生成对应的缓存key值 4 key_md5 = encrypt_helper.md5(key) 5 # 从缓存中提取菜单记录 6 model = cache_helper.get(key_md5) 7 # 记录不存在时,运行记录载入缓存程序 8 if not model: 9 self._load_cache() 10 model = cache_helper.get(key_md5) 11 return model 12 13 def _load_cache(self): 14 """全表记录载入缓存""" 15 # 生成缓存载入状态key,主要用于检查是否已执行了菜单表载入缓存判断 16 cache_key = self.__table_name + '_is_load' 17 # 将自定义的key存储到全局缓存队列中(关于全局缓存队列请查看前面ORM对应章节说明) 18 self.add_relevance_cache_in_list(cache_key) 19 # 获取缓存载入状态,检查记录是否已载入缓存,是的话则不再执行 20 if cache_helper.get(cache_key): 21 return 22 # 从数据库中读取全部记录 23 result = self.get_list() 24 # 标记记录已载入缓存 25 cache_helper.set(cache_key, True) 26 # 如果菜单表没有记录,则直接退出 27 if not result: 28 return 29 # 循环遍历所有记录,组合处理后,存储到nosql缓存中 30 for model in result.get('rows', {}): 31 # 提取菜单页面对应的接口(后台菜单管理中的接口值,同一个菜单操作时,经常需要访问多个接口,所以这个值有中存储多们接口值) 32 interface_url = model.get('interface_url', '') 33 if not interface_url: 34 continue 35 # 获取前端html页面地址 36 page_url = model.get('page_url', '') 37 38 # 同一页面接口可能有多个,所以需要进行分割 39 interface_url_arr = interface_url.replace('\n', '').replace(' ', '').split(',') 40 # 逐个接口处理 41 for interface in interface_url_arr: 42 # html+接口组合生成key 43 url_md5 = encrypt_helper.md5(page_url + interface) 44 # 存储到全局缓存队列中,方便菜单记录更改时,自动清除这些自定义缓存 45 self.add_relevance_cache_in_list(url_md5) 46 # 存储到nosql缓存 47 cache_helper.set(url_md5, model)
这里的权限管理逻辑有点绕,需要认真思考与debug检查,才能真正掌握。另外,也可以通过后台菜单管理中,故意修改菜单项的某些值,来检查这里的代码处理与变化。
完成以上代码以后,权限的处理就完成了,接下来只需要在每个后台管理接口中添加下面代码就可以做到接口的访问权限控制了。
@get('/api/main/menu_info/') def callback(): """ 主页面获取菜单列表数据 """ # 检查用户权限 _common_logic.check_user_power()
具体大家可以查看文章后面提供的源码,看看后台管理接口处理就清楚了。
版权声明:本文原创发表于 博客园,作者为 AllEmpty 本文欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则视为侵权。
python开发QQ群:669058475 作者博客:http://www.cnblogs.com/EmptyFS/