個人第一個python web開發框架(40)——後臺日誌與異常處理

  後臺權限和底層框架的改造終於完成了,小白也終於能夠放下緊懸着的心,能夠輕鬆一下了。這不他爲了感謝老菜,又找老菜聊了起來。python

  小白:多謝老大的幫忙,系統終於改造完成了,能夠好好放鬆一下了。web

  老菜:呵呵,對於後臺管理系統功能,你以爲已經完工了嗎?沒有什麼遺漏的嗎?數據庫

  小白:啊......權限管理完成後不就完了嗎?還有功能要弄的嗎?api

  老菜:若是光從使用角度來講,也可能說完成了,但還有一些細節還須要處理的,好比說日誌和異常。緩存

  小白:前面不是作過日誌處理了,將全部的異常都自動寫到日誌中,方便開發人員分析查看,還能自動發送異常通知郵件,另外對於客戶端提交的全部數據,在bottle勾子那裏也作了處理,都寫入到日誌中了,還有什麼要處理的?安全

  老菜:對於日誌來講能夠分爲兩塊:服務器

  一是管理員的操做日誌,由於後臺管理操做涉及到數據安全,管理員的全部操做都須要記錄下來,以便發生問題時能夠找到關係人,同時有些業務系統交給相關人員使用之後,BOSS殊不知道他們到底有沒有登陸使用,天天在系統作什麼;微信

  二是系統的異常和關鍵數據的記錄,這個屬於系統底層的日誌,將全部異常和與金錢相關的操做信息所有記錄下來,有故障時開發人員能夠根據日誌快速定位,及時修復問題。這方面咱們前面已經作一部分了,在前面底層不少地方都作了try...except...處理,這是很必要的,但你有沒有發現,咱們的代碼在本地常常運行的好好的,而將代碼更新上服務器後即常常爆500錯誤殊不知道,想要排查異常時也很不方便,但查看uwsgi等多個系統日誌才行,有些異常你查來查去都查不出來,很是浪費時間,你清楚這些異常主要是由什麼引發的嗎?有沒有想過用什麼方法也能夠作到實時經過推送通知了解這些錯誤呢?固然對於異常的發生是很難避免的,可是咱們能夠經過一些手段,讓這些異常發生後即時經過郵件或微信等方式,將異常詳情通知咱們,而後快速修復問題。若是你對系統很是熟悉的話,有可能用戶還沒反應過來,十幾秒你就將故障修復了,作到人不知鬼不覺,哈哈。session

  小白:是啊,異常問題是我最大痛的事情,不少時候明明本地調試的好好的,一到服務就掛了,找到找去也找不出問題所在,浪費了大量的時間。那麼咱們要怎麼來進行改造呢?數據結構

  老菜:接下來你看我講解就知道了,主要是對已有代碼進行修改。

 

  在前面的數據結構設計時,咱們有一個管理員操做日誌表,接下來的改造主要是對這個表進行相關的操做。

  首先咱們須要建立這個日誌表的邏輯類,因爲咱們的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, '登陸成功')
View Code

  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
View Code

 

  想要記錄用戶的每個操做記錄,有兩種方法,一是在每一個接口那裏添加日誌記錄,這樣能夠更詳細的編寫自定義日誌說明,不過這樣作的話工做量會比較大,也容易在複製粘貼中出錯;還有就是,每個後臺接口都會調用權限判斷方法,咱們也能夠在這個方法中直接添加日誌記錄,缺點就是每一個訪問操做想要說明的很細緻很難作到,這裏咱們經過各類判斷與組合方式,來寫入對應的接口日誌訪問記錄,不免會出現記錄重複或記錄說明不正確的狀況。

  下面是後臺權限檢查方法(_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時,接收異常郵件推送了。很是方便好用。

 

  本文對應的源碼下載 

 

版權聲明:本文原創發表於 博客園,做者爲 AllEmpty 本文歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然視爲侵權。

python開發QQ羣:669058475(本羣已滿)、733466321(能夠加2羣)    做者博客:http://www.cnblogs.com/EmptyFS/

相關文章
相關標籤/搜索