后台權限和底層框架的改造終於完成了,小白也終於可以放下緊懸着的心,可以輕松一下了。這不他為了感謝老菜,又找老菜聊了起來。
小白:多謝老大的幫忙,系統終於改造完成了,可以好好放松一下了。
老菜:呵呵,對於后台管理系統功能,你覺得已經完工了嗎?沒有什么遺漏的嗎?
小白:啊......權限管理完成后不就完了嗎?還有功能要弄的嗎?
老菜:如果光從使用角度來說,也可能說完成了,但還有一些細節還需要處理的,比如說日志和異常。
小白:前面不是做過日志處理了,將所有的異常都自動寫到日志中,方便開發人員分析查看,還能自動發送異常通知郵件,另外對於客戶端提交的所有數據,在bottle勾子那里也做了處理,都寫入到日志中了,還有什么要處理的?
老菜:對於日志來說可以分為兩塊:
一是管理員的操作日志,因為后台管理操作涉及到數據安全,管理員的所有操作都需要記錄下來,以便發生問題時可以找到關系人,同時有些業務系統交給相關人員使用以后,BOSS卻不知道他們到底有沒有登錄使用,每天在系統做什么;
二是系統的異常和關鍵數據的記錄,這個屬於系統底層的日志,將所有異常和與金錢相關的操作信息全部記錄下來,有故障時開發人員可以根據日志快速定位,及時修復問題。這方面我們前面已經做一部分了,在前面底層很多地方都做了try...except...處理,這是很必要的,但你有沒有發現,我們的代碼在本地經常運行的好好的,而將代碼更新上服務器后即經常爆500錯誤卻不知道,想要排查異常時也很不方便,但查看uwsgi等多個系統日志才行,有些異常你查來查去都查不出來,非常浪費時間,你清楚這些異常主要是由什么引起的嗎?有沒有想過用什么方法也可以做到實時通過推送通知了解這些錯誤呢?當然對於異常的發生是很難避免的,但是我們可以通過一些手段,讓這些異常發生后即時通過郵件或微信等方式,將異常詳情通知我們,然后快速修復問題。如果你對系統非常熟悉的話,有可能用戶還沒反應過來,十幾秒你就將故障修復了,做到人不知鬼不覺,哈哈。
小白:是啊,異常問題是我最大痛的事情,很多時候明明本地調試的好好的,一到服務就掛了,找到找去也找不出問題所在,浪費了大量的時間。那么我們要怎么來進行改造呢?
老菜:接下來你看我講解就知道了,主要是對已有代碼進行修改。
在前面的數據結構設計時,我們有一個管理員操作日志表,接下來的改造主要是對這個表進行相關的操作。
首先我們需要創建這個日志表的邏輯類,由於我們的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時,接收異常郵件推送了。非常方便好用。
版權聲明:本文原創發表於 博客園,作者為 AllEmpty 本文歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則視為侵權。
python開發QQ群:669058475(本群已滿)、733466321(可以加2群) 作者博客:http://www.cnblogs.com/EmptyFS/