日志日志logging
在前面的数据结构设计时,我们有一个管理员操作日志表,接下来的改造主要是对这个表进行相关的操作。
首先我们需要创建这个日志表的逻辑类,由于我们的ORM是用字典来进行增改操作的,所以需要先组合字段字典,然后再执行对应的方法,为了让操作简化,我们需要在日志表逻辑类中添加一个方法,通过传参的方式来进行日志的添加操作,这样就可以免去我们组合字典的操作了。
1 #!/usr/bin/env python
2 # coding=utf-8
3
4 from logic import _logic_base
5 from common.string_helper import string
6 from config import db_config
7
8
9 class ManagerOperationLogLogic(_logic_base.LogicBase):
10 """管理员操作日志管理表逻辑类"""
11
12 def __init__(self):
13 # 表名称
14 self.__table_name = 'manager_operation_log'
15 # 初始化
16 _logic_base.LogicBase.__init__(self, db_config.DB, db_config.IS_OUTPUT_SQL, self.__table_name)
17
18
19 def add_operation_log(self, manager_id, manager_name, ip, remark):
20 """记录用户登录日志"""
21 # 组合要更新的字段内容
22 fields = {'manager_id':manager_id, 'manager_name':string(manager_name), 'ip':string(ip), 'remark':string(remark)}
23 # 新增记录
24 self.add_model(fields)
从代码中可以看到,add_operation_log()方法,它其实就是将要更新到数据库的参数传进来,在方法里组合成字典,然后调用add_model()进行更新操作,调用时用下面代码就可以了
_manager_operation_log_logic.add_operation_log(manager_id, manager_name, ip, '登陆成功')
完成这个操作日志逻辑类和日志添加方法以后,要改造登录接口就简单多了,只需要在出错(密码错误、禁用)和成功时进行调用,记录到数据表就可以了,具体看代码。
登录接口除了需要添加日志记录以外,还需要处理一个安全问题,我们没有对多次输出密码错误进行处理,如果有人想要登录系统写个密码劳举器,可能很容易后台就给人攻破了,所以我们需要对这个做一个限制,比如说同一ip在指定时间内只能出错多少次,每次出错时都记录一下出错次数,当出错次数超出限制时,则拒绝用户登录。具体自行查看代码,这里我就不再详细说明了。
1 #!/usr/bin/env python
2 # coding=utf-8
3
4 from bottle import put
5 from common import web_helper, encrypt_helper, security_helper
6 from common.string_helper import string
7 from logic import manager_logic, manager_operation_log_logic
8
9
10 @put('/api/login/')
11 def post_login():
12 """用户登陆验证"""
13 ##############################################################
14 # 获取并验证客户端提交的参数
15 ##############################################################
16 username = web_helper.get_form('username', '帐号')
17 password = web_helper.get_form('password', '密码')
18 verify = web_helper.get_form('verify', '验证码')
19 ip = web_helper.get_ip()
20
21 ##############################################################
22 # 从session中读取验证码信息
23 ##############################################################
24 s = web_helper.get_session()
25 verify_code = s.get('verify_code')
26 # 删除session中的验证码(验证码每提交一次就失效)
27 if 'verify_code' in s:
28 del s['verify_code']
29 s.save()
30 # 判断用户提交的验证码和存储在session中的验证码是否相同
31 if verify.upper() != verify_code:
32 return web_helper.return_msg(-1, '验证码错误')
33
34 ##############################################################
35 ### 判断用户登录失败次数,超出次做登录限制 ###
36 # 获取管理员登录密码错误限制次数,0=无限制,x次/小时
37 limit_login_count = 10
38 # 获取操作出错限制值
39 is_ok, msg, operation_times_key, error_count = security_helper.check_operation_times('login_error_count', limit_login_count, False)
40 # 判断操作的出错次数是否已超出了限制
41 if not is_ok:
42 return web_helper.return_msg(-1, msg)
43
44 ##############################################################
45 ### 获取登录用户记录,并进行登录验证 ###
46 ##############################################################
47 # 初始化操作日志记录类
48 _manager_operation_log_logic = manager_operation_log_logic.ManagerOperationLogLogic()
49 # 初始化管理员逻辑类
50 _manager_logic = manager_logic.ManagerLogic()
51 # 从数据库中读取用户信息
52 manager_result = _manager_logic.get_model_for_cache_of_where('login_name=' + string(username))
53 # 判断用户记录是否存在
54 if not manager_result:
55 return web_helper.return_msg(-1, '账户不存在')
56
57 # 获取管理员id
58 manager_id = manager_result.get('id', 0)
59 # 获取管理员姓名
60 manager_name = manager_result.get('name', '')
61
62 ##############################################################
63 ### 验证用户登录密码与状态 ###
64 ##############################################################
65 # 对客户端提交上来的验证进行md5加密将转为大写(为了密码的保密性,这里进行双重md5加密,加密时从第一次加密后的密串中提取一段字符串出来进行再次加密,提取的串大家可以自由设定)
66 # pwd = encrypt_helper.md5(encrypt_helper.md5(password)[1:30]).upper()
67 # 对客户端提交上来的验证进行md5加密将转为大写(只加密一次)
68 pwd = encrypt_helper.md5(password).upper()
69 # 检查登录密码输入是否正确
70 if pwd != manager_result.get('login_password').upper():
71 # 记录出错次数
72 security_helper.add_operation_times(operation_times_key)
73 # 记录日志
74 _manager_operation_log_logic.add_operation_log(manager_id, manager_name, ip, '【' + manager_name + '】输入的登录密码错误')
75 return web_helper.return_msg(-1, '密码错误')
76 # 检查该账号虽否禁用了
77 if not manager_result.get('is_enabled'):
78 # 记录出错次数
79 security_helper.add_operation_times(operation_times_key)
80 # 记录日志
81 _manager_operation_log_logic.add_operation_log(manager_id, manager_name, ip, '【' + manager_name + '】账号已被禁用,不能登录系统')
82 return web_helper.return_msg(-1, '账号已被禁用')
83
84 # 登录成功,清除登录错误记录
85 security_helper.del_operation_times(operation_times_key)
86
87 ##############################################################
88 ### 把用户信息保存到session中 ###
89 ##############################################################
90 manager_id = manager_result.get('id')
91 s['id'] = manager_id
92 s['login_name'] = username
93 s['name'] = manager_result.get('name')
94 s['positions_id'] = manager_result.get('positions_id')
95 s.save()
96
97 ##############################################################
98 ### 更新用户信息到数据库 ###
99 ##############################################################
100 # 更新当前管理员最后登录时间、Ip与登录次数(字段说明,请看数据字典)
101 fields = {
102 'last_login_time': 'now()',
103 'last_login_ip': string(ip),
104 'login_count': 'login_count+1',
105 }
106 # 写入数据库
107 _manager_logic.edit_model(manager_id, fields)
108 # 记录日志
109 _manager_operation_log_logic.add_operation_log(manager_id, manager_name, ip, '【' + manager_name + '】登陆成功')
110
111 return web_helper.return_msg(0, '登录成功')
security_helper.py代码
1 #!/usr/bin/env python 2 # coding=utf-8 3 4 from common import cache_helper, convert_helper, encrypt_helper 5 6 7 def check_operation_times(operation_name, limiting_frequency, ip, is_add=True): 8 """ 9 检查操作次数 10 参数: 11 operation_name 操作名称 12 limiting_frequency 限制次数 13 is_add 是否累加 14 返回参数: 15 True 不限制 16 False 限制操作 17 """ 18 if not operation_name or limiting_frequency is None: 19 return False, '参数错误,错误码:-400-001,请与管理员联系', '', 0 20 21 # 如果限制次数为0时,默认不限制操作 22 if limiting_frequency <= 0: 23 return True, '', '', 0 24 25 ############################################################## 26 ### 判断用户操作次数,超出次数限制执行 ### 27 # 获取当前用户已记录操作次数 28 operation_times_key = operation_name + '_' + encrypt_helper.md5(operation_name + ip) 29 operation_times = convert_helper.to_int0(cache_helper.get(operation_times_key)) 30 31 # 如果系统限制了出错次数,且当前用户已超出限制,则返回错误 32 if limiting_frequency and operation_times >= limiting_frequency: 33 return False, '您在10分钟内连续操作次数达到' + str(limiting_frequency) + '次,已超出限制,请稍候再试', operation_times_key, operation_times 34 35 if is_add: 36 # 记录操作次数,默认在缓存中存储10分钟 37 cache_helper.set(operation_times_key, operation_times + 1, 600) 38 39 return True, '', operation_times_key, operation_times 40 41 42 def add_operation_times(operation_times_key): 43 """ 44 累加操作次数 45 参数: 46 operation_times_key 缓存key 47 """ 48 # 获取当前用户已记录操作次数 49 get_operation_times = convert_helper.to_int0(cache_helper.get(operation_times_key)) 50 # 记录获取次数 51 cache_helper.set(operation_times_key, get_operation_times + 1, 600) 52 53 54 def del_operation_times(operation_times_key): 55 """ 56 清除操作次数 57 参数: 58 operation_times_key 缓存key 59 """ 60 # 记录获取次数 61 cache_helper.delete(operation_times_key) 62 63 64 def check_login_power(id, k, t, sessionid): 65 """ 66 检查拨号小信接口,验证用户是否有权限访问 67 :param id: 用户id 68 :param k: 32位长度的密钥串 69 :param t: 时间戳 70 :param sessionid: 当前用户的密钥 71 :return: False=验证失败,True=验证成功 72 """ 73 if not sessionid: 74 return False 75 76 return encrypt_helper.md5(str(id) + sessionid + str(t) + sessionid + str(id)) == k
想要记录用户的每一个操作记录,有两种方法,一是在每个接口那里添加日志记录,这样可以更详细的编写自定义日志说明,不过这样做的话工作量会比较大,也容易在复制粘贴中出错;还有就是,每一个后台接口都会调用权限判断方法,我们也可以在这个方法中直接添加日志记录,缺点就是每个访问操作想要说明的很细致很难做到,这里我们通过各种判断与组合方式,来写入对应的接口日志访问记录,难免会出现记录重复或记录说明不正确的情况。
下面是后台权限检查方法(_common_logic.py)
1 #!/usr/bin/env python
2 # coding=utf-8
3
4 from bottle import request
5 from common import web_helper, string_helper
6 from logic import menu_info_logic, positions_logic, manager_operation_log_logic
7
8 def check_user_power():
9 """检查当前用户是否有访问当前接口的权限"""
10 # 读取session
11 session = web_helper.get_session()
12 # session不存在则表示登录失效了
13 if not session:
14 web_helper.return_raise(web_helper.return_msg(-404, "您的登录已失效,请重新登录"))
15
16 # 获取当前页面原始路由
17 rule = request.route.rule
18 # 获取当前访问接口方式(get/post/put/delete)
19 method = request.method.lower()
20 # 获取当前访问的url地址
21 url = string_helper.filter_str(request.url, '<|>|%|\'')
22
23 # 初始化日志相关变量
24 _manager_operation_log_logic = manager_operation_log_logic.ManagerOperationLogLogic()
25 ip = web_helper.get_ip()
26 manager_id = session.get('id')
27 manager_name = session.get('name')
28 # 设置访问日志信息
29 if method == 'get':
30 method_name = '访问'
31 else:
32 method_name = '进行'
33
34 # 获取来路url
35 http_referer = request.environ.get('HTTP_REFERER')
36 if http_referer:
37 # 提取页面url地址
38 index = http_referer.find('?')
39 if index == -1:
40 web_name = http_referer[http_referer.find('/', 8) + 1:]
41 else:
42 web_name = http_referer[http_referer.find('/', 8) + 1: index]
43 else:
44 web_name = ''
45
46 # 组合当前接口访问的缓存key值
47 key = web_name + method + '(' + rule + ')'
48 # 从菜单权限缓存中读取对应的菜单实体
49 _menu_info_logic = menu_info_logic.MenuInfoLogic()
50 model = _menu_info_logic.get_model_for_url(key)
51 if not model:
52 # 添加访问失败日志
53 _manager_operation_log_logic.add_operation_log(manager_id, manager_name, ip, '用户访问[%s]接口地址时,检测没有操作权限' % (url))
54 web_helper.return_raise(web_helper.return_msg(-1, "您没有访问权限1" + key))
55
56 # 初始化菜单名称
57 menu_name = model.get('name')
58 if model.get('parent_id') > 0:
59 # 读取父级菜单实体
60 parent_model = _menu_info_logic.get_model_for_cache(model.get('parent_id'))
61 if parent_model:
62 menu_name = parent_model.get('name').replace('列表', '').replace('管理', '') + menu_name
63
64 # 从session中获取当前用户登录时所存储的职位id
65 positions = positions_logic.PositionsLogic()
66 page_power = positions.get_page_power(session.get('positions_id'))
67 # 从菜单实体中提取菜单id,与职位权限进行比较,判断当前用户是否拥有访问该接口的权限
68 if page_power.find(',' + str(model.get('id', -1)) + ',') == -1:
69 # 添加访问失败日志
70 _manager_operation_log_logic.add_operation_log(manager_id, manager_name, ip, '用户%s[%s]操作检测没有权限' % (method_name, menu_name))
71 web_helper.return_raise(web_helper.return_msg(-1, "您没有访问权限2"))
72
73 if not (method == 'get' and model.get('name') in ('添加', '编辑')):
74 # 添加访问日志
75 _manager_operation_log_logic.add_operation_log(manager_id, manager_name, ip, '用户%s[%s]操作' % (method_name, menu_name))
这里记录的日志与菜单管理记录相关,如果菜单项的命名或树列表不规范,则记录的日志可能就会偏差比较大。当然如果你有强迫症追求完美的话,可自行对它进行改造。比如说在菜单管理中添加一个字段,用来编写日志说明的,访问这个页面时直接将说明更新到操作日志表中就可以了,简单方便。而如果对操作内容想要更细致的,也可以在日志表中添加一个字段,将客户端提交的参数全部写入到字段里记录,这样对用户的操作就会更清晰了,当然如果用户更新新闻或文章类内容时,字段值也会比较大。大家可以根据需要来进行对应改造。
下图为操作日志表记录内容

后台管理还需要做个日志查看的页面,接口代码很简单,具体直接看源码,这里也不详细说明了

对于异常处理,大家其实都知道使用try...except...进行捕捉,然后记录异常信息或作对应处理。
而在接口发生500错误时,由于程序在服务器端执行,服务器环境与本地的开发环境有所不同,就很难直观的判断是什么原因引起的,可能是少上传了某个调用文件,也可能是新引用的包没有安装,又或者是代码中写错了代码,也有可能是变量为空引起的异常,反正可能情况非常之多,当接口非常多时,这些异常通过很隐蔽,只有等到该接口被调用时才能发现,如果处理不好,开发人员可能会花费不少时间在这上面。
当然也有办法是,所有的接口代码都放在try...except...里面执行,这样发生500的情况会大大减少,但代码看起来层级多了也不美观。对于这种简单重复统一的代码,python有一个非常好用的工具,那就是装饰器,我们可以编写一个装饰器方法给接口使用,从而实现我们想要的目的。
装饰器实现的原理就是,通过在函数头部引用装饰器,从而使程序执行代码时,先执行装饰器里面的代码,然后再调用引用装饰器的函数,最后再返回装饰器执行剩下的代码。简单的理解就是,原有A函数和装饰器B函数,当A函数引用装饰器B函数以后,A函数其实就变成B函数中被调用的一个方法,即B函数在执行过程中会调用A函数,执行完成A函数后返回想要的结果再继续执行后面的代码
先上代码,我们在异常操作包中(except_helper.py),添加下面方法:
1 def exception_handling(func): 2 """接口异常处理装饰器""" 3 def wrapper(*args, **kwargs): 4 try: 5 # 执行接口方法 6 return func(*args, **kwargs) 7 except Exception as e: 8 # 捕捉异常,如果是中断无返回类型操作,则再执行一次 9 if isinstance(e, HTTPResponse): 10 func(*args, **kwargs) 11 # 否则写入异常日志,并返回错误提示 12 else: 13 log_helper.error(str(e.args)) 14 return web_helper.return_msg(-1, "操作失败") 15 return wrapper
func就是注入到装饰器方法中的其他方法,由于我们的装饰器是给接口使用,所以执行过程中直接返回结果(见第6行代码),由于我们的代码在执行过程,有时会调用raise来中断代码执行,这样的话接口方法是没有返回值的,如果使用return来调用方法就会出现异常,所以在第9到10行,会调用方法重新执行一次接口方法,所以在开发时要注意,只有对那些出错时需要马上中断的地方,才使用raise这样保证重复执行接口方法不会造成数据错误。
当接口方法执行出现异常要抛出500时,这个装饰器就会捕捉到,然后通过调用log_helper.error()方法,将异常写入日志,并发送异常通知邮件通知开发人员。对于异常通知,如果你注册了微信企业号,你可以编写对应的代码与企业号进行对接,让你和相关人员在微信上可以实时接收到异常推送消息,方便即时发现问题然后处理问题。
下面是调用方法:
1 @get('/api/system/department/<id:int>/')
2 @exception_handling
3 def callback(id):
4 """
5 获取指定记录
6 """
7 # 检查用户权限
8 _common_logic.check_user_power()
9
10 _department_logic = department_logic.DepartmentLogic()
11 # 读取记录
12 result = _department_logic.get_model_for_cache(id)
13 if result:
14 return web_helper.return_msg(0, '成功', result)
15 else:
16 return web_helper.return_msg(-1, "查询失败")
只需要在接口路由和接口方法之间,添加@exception_handling就可以实现接口500时,接收异常邮件推送了。非常方便好用。
本文来自博客园,作者:南鱼羁荒渡,转载请注明原文链接:https://www.cnblogs.com/nanyu/articles/9647202.html

浙公网安备 33010602011771号