要完成后台管理系統登錄功能,通過查看登錄頁面,我們可以了解到,我們需要編寫驗證碼圖片獲取接口和登錄處理接口,然后在登錄頁面的HTML上編寫AJAX。
在進行接口開發之前,還有一個重要的事情要處理,那就是對站點進行初始化,如果不進行初始化,那么獨立文件編寫的接口將會找不到,要將異常錯誤寫入日志文件也會找不到路徑,下面先上代碼。
打開main.py文件,改為下面代碼(大家可以比較一下和之前代碼有什么不同)
1 #!/usr/bin/evn python
2 # coding=utf-8
3
4 import bottle
5 import sys
6 import os
7 import logging
8 import urllib.parse
9 from bottle import default_app, get, run, request, hook
10 from beaker.middleware import SessionMiddleware
11
12 # 導入工具函數包
13 from common import web_helper, log_helper
14 # 導入api代碼模塊(初始化api文件夾里的各個訪問路由,這一句不能刪除,刪除后將無法訪問api文件夾里的各個接口)
15 import api
16
17 #############################################
18 # 初始化bottle框架相關參數
19 #############################################
20 # 獲取當前main.py文件所在服務器的絕對路徑
21 program_path = os.path.split(os.path.realpath(__file__))[0]
22 # 將路徑添加到python環境變量中
23 sys.path.append(program_path)
24 # 讓提交數據最大改為2M(如果想上傳更多的文件,可以在這里進行修改)
25 bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 * 2
26
27 #############################################
28 # 初始化日志相關參數
29 #############################################
30 # 如果日志目錄log文件夾不存在,則創建日志目錄
31 if not os.path.exists('log'):
32 os.mkdir('log')
33 # 初始化日志目錄路徑
34 log_path = os.path.join(program_path, 'log')
35 # 定義日志輸出格式與路徑
36 logging.basicConfig(level=logging.INFO,
37 format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
38 filename="%s/info.log" % log_path,
39 filemode='a')
40
41 # 設置session參數
42 session_opts = {
43 'session.type': 'file',
44 'session.cookie_expires': 3600,
45 'session.data_dir': '/tmp/sessions/simple',
46 'session.auto': True
47 }
48
49
50 @hook('before_request')
51 def validate():
52 """使用勾子處理接口訪問事件"""
53
54 # 獲取當前訪問的Url路徑
55 path_info = request.environ.get("PATH_INFO")
56 # 過濾不用做任何操作的路由(即過濾不用進行判斷是否登錄和記錄日志的url)
57 if path_info in ['/favicon.ico', '/', '/api/verify/']:
58 return
59 ### 記錄客戶端提交的參數 ###
60 # 獲取當前訪問url路徑與ip
61 request_log = 'url:' + path_info + ' ip:' + web_helper.get_ip()
62 try:
63 # 添加json方式提交的參數
64 if request.json:
65 request_log = request_log + ' params(json):' + urllib.parse.unquote(str(request.json))
66 except:
67 pass
68 try:
69 # 添加GET方式提交的參數
70 if request.query_string:
71 request_log = request_log + ' params(get):' + urllib.parse.unquote(str(request.query_string))
72 # 添加POST方式提交的參數
73 if request.method == 'POST':
74 request_log = request_log + ' params(post):' + urllib.parse.unquote(str(request.params.__dict__))
75 # 存儲到日志文件中
76 log_helper.info(request_log)
77 except:
78 pass
79
80 # 處理ajax提交的put、delete等請求轉換為對應的請求路由(由於AJAX不支持RESTful風格提交,所以需要在這里處理一下,對提交方式進行轉換)
81 if request.method == 'POST' and request.POST.get('_method'):
82 request.environ['REQUEST_METHOD'] = request.POST.get('_method', '')
83
84 # 過濾不用進行登錄權限判斷的路由(登錄與退出登錄不用檢查是否已經登錄)
85 url_list = ["/api/login/", "/api/logout/"]
86 if path_info in url_list:
87 pass
88 else:
89 # 已經登錄成功的用戶session肯定有值,沒有值的就是未登錄
90 session = web_helper.get_session()
91 # 獲取用戶id
92 manager_id = session.get('id', 0)
93 login_name = session.get('login_name', 0)
94 # 判斷用戶是否登錄
95 if not manager_id or not login_name:
96 web_helper.return_raise(web_helper.return_msg(-404, "您的登錄已失效,請重新登錄"))
97
98
99
100 # 函數主入口
101 if __name__ == '__main__':
102 app_argv = SessionMiddleware(default_app(), session_opts)
103 run(app=app_argv, host='0.0.0.0', port=9090, debug=True, reloader=True)
104 else:
105 # 使用uwsgi方式處理python訪問時,必須要添加這一句代碼,不然無法訪問
106 application = SessionMiddleware(default_app(), session_opts)
main.py文件里有詳細的注釋說明,所以不進行細說,在這里講一講文件大體的思路。
因為我們編寫的接口文件都放在api文件夾中,當web服務啟動后需要將api里的接口文件自動裝載進來,讓我們可以通過url訪問里面的接口,所以需要在main.py這個入口函數中,對api文件夾里的接口文件進行導入,前面講解到我們api文件夾里有一個__init__.py文件,它會自動幫我們導入當前文件夾里的所有文件,所以我們只需要在main.py中添加import api這一行代碼就可以了。
另外,我們需要告訴python服務當前程序所在的路徑,所以需要將當前文件所在的絕對路徑添加到python環境變量中(第21到23行)
我們要記錄異常信息到日志,要記錄客戶端訪問的url與提交的請求參數,方便出錯時幫助我們進行排查錯誤,所以要初始化日志文件格式與存儲路徑(第30到39行)
bottle框架有兩個好用的勾子處理函數(具體流程如下圖),客戶端訪問接口時,首先會從bottle web服務綁定的入口進入,然后調用before_request這個勾子函數(第50到97行),執行完里面的代碼后再進入對應的接口函數里,當接口函數運行完畢后,又會調用after_request這個勾子函數(我們使用了nginx處理前端訪問服務不存在跨域問題,所以main.py就沒有添加這個勾子函數),運行完里面的代碼后才返回最終結果給客戶端。所以我們有很多事情可以放在這兩個勾子函數中進行處理。before_request中我們可以運行初始化操作、記錄客戶端訪問的url與提交的請求參數操作、判斷用戶是否已經登錄等操作(如果沒有這個勾子函數,我們要判斷用戶是否登錄,就必須在每個接口文件中處理,這樣一方面代碼會很冗余,出現大量重復的沒有必要的代碼,另一方面也很容易出錯或遺漏掉,造成后端權限訪問漏洞。而after_request這個函數通過是用來處理輸出HTTP頭信息等內容,比如跨域處理等。

第55到78行,會將客戶端訪問的url與各種方式提交的請求參數記錄到日志。對於一些不想記錄到日志的訪問,可以添加到第57行。(如下圖)

第90到96行,對登錄用戶訪問進行處理,如果未登錄的,則會返回-404狀態,客戶端的ajax接收到這個狀態后,自行處理跳轉到登錄頁面。
驗證碼接口
我們在api文件夾中創建verify.py文件
#!/usr/bin/python
#coding: utf-8
from io import BytesIO
from bottle import get, response
from common import verify_helper, log_helper, web_helper
@get('/api/verify/')
def get_verify():
"""生成驗證碼圖片"""
try:
# 獲取生成驗證碼圖片與驗證碼
code_img, verify_code = verify_helper.create_verify_code()
# 將字符串轉化成大寫保存到session中
s = web_helper.get_session()
s['verify_code'] = verify_code.upper()
s.save()
# 輸出圖片流
buffer = BytesIO()
code_img.save(buffer, "jpeg")
code_img.close()
response.set_header('Content-Type', 'image/jpg')
return buffer.getvalue()
except Exception as e:
log_helper.error(str(e.args))
code_img, verify_code = verify_helper.create_verify_code() :運行verify_helper.create_verify_code() ,會返回圖片流和驗證碼,python語言執行函數后,可以直接返回字符串、數值、元組、字典、列表等各種類型的值,返回元組類型值時,就可以使用這樣的方式進行接收。
log_helper.error(str(e.args)) 這是我們前面工具函數包時所講到的錯誤記錄函數,當生成驗證碼出現異常時,它會將異常信息記錄到日志文件中,並將異常發送到我們指定的郵箱。
添加完這個文件后,我們就可以運行一下main.py,然后在瀏覽器中輸入http://127.0.0.1:9090/api/verify/或http://127.0.0.1:81/api/verify/,就可以看到生成的驗證碼了(如果使用81端口無法訪問)

登錄接口
我們在api文件夾中創建login.py文件
1 #!/usr/bin/evn python
2 # coding=utf-8
3
4 from bottle import put
5 from common import web_helper, encrypt_helper, db_helper
6
7
8 @put('/api/login/')
9 def post_login():
10 """用戶登陸驗證"""
11 ##############################################################
12 # 獲取並驗證客戶端提交的參數
13 ##############################################################
14 username = web_helper.get_form('username', '帳號')
15 password = web_helper.get_form('password', '密碼')
16 verify = web_helper.get_form('verify', '驗證碼')
17 ip = web_helper.get_ip()
18
19 ##############################################################
20 # 從session中讀取驗證碼信息
21 ##############################################################
22 s = web_helper.get_session()
23 verify_code = s.get('verify_code')
24 # 刪除session中的驗證碼(驗證碼每提交一次就失效)
25 if 'verify_code' in s:
26 del s['verify_code']
27 s.save()
28 # 判斷用戶提交的驗證碼和存儲在session中的驗證碼是否相同
29 if verify.upper() != verify_code:
30 return web_helper.return_msg(-1, '驗證碼錯誤')
31
32 ##############################################################
33 ### 獲取登錄用戶記錄,並進行登錄驗證 ###
34 ##############################################################
35 sql = """select * from manager where login_name='%s'""" % (username,)
36 # 從數據庫中讀取用戶信息
37 manager_result = db_helper.read(sql)
38 # 判斷用戶記錄是否存在
39 if not manager_result:
40 return web_helper.return_msg(-1, '賬戶不存在')
41
42 ##############################################################
43 ### 驗證用戶登錄密碼與狀態 ###
44 ##############################################################
45 # 對客戶端提交上來的驗證進行md5加密將轉為大寫(為了密碼的保密性,這里進行雙重md5加密,加密時從第一次加密后的密串中提取一段字符串出來進行再次加密,提取的串大家可以自由設定)
46 # pwd = encrypt_helper.md5(encrypt_helper.md5(password)[1:30]).upper()
47 # 對客戶端提交上來的驗證進行md5加密將轉為大寫(只加密一次)
48 pwd = encrypt_helper.md5(password).upper()
49 # 檢查登錄密碼輸入是否正確
50 if pwd != manager_result[0].get('login_password', ''):
51 return web_helper.return_msg(-1, '密碼錯誤')
52 # 檢查該賬號雖否禁用了
53 if manager_result[0].get('is_enable', 0) == 0:
54 return web_helper.return_msg(-1, '賬號已被禁用')
55
56 ##############################################################
57 ### 把用戶信息保存到session中 ###
58 ##############################################################
59 manager_id = manager_result[0].get('id', 0)
60 s['id'] = manager_id
61 s['login_name'] = username
62 s.save()
63
64 ##############################################################
65 ### 更新用戶信息到數據庫 ###
66 ##############################################################
67 # 更新當前管理員最后登錄時間、Ip與登錄次數(字段說明,請看數據字典)
68 sql = """update manager set last_login_time=%s, last_login_ip=%s, login_count=login_count+1 where id=%s"""
69 # 組合更新值
70 vars = ('now()', ip, manager_id,)
71 # 寫入數據庫
72 db_helper.write(sql, vars)
73
74 return web_helper.return_msg(0, '登錄成功')
在編寫登錄接口前,我們首先要了解登錄接口處理的流程是怎么樣的

login.py后台登錄處理接口代碼可以看到,路由我們使用的是@put('/api/login/'),RESTful風格中,post是用於新增記錄,put是用於修改或改變服務器數據,登錄我理解它肯定不是新增,它是改變用戶登錄的狀態,所以這里使用put方式接收
登錄接口的代碼有詳細的注釋,還有上面的流程圖,所以就不再深入解說,大家自己看代碼,如有不明白的,文章后面留言。
前端登錄html頁面(login.html)
1 <!DOCTYPE HTML>
2 <html>
3 <head>
4 <meta charset="utf-8">
5 <meta name="renderer" content="webkit|ie-comp|ie-stand">
6 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
7 <meta name="viewport"
8 content="width=device-width,initial-scale=1,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"/>
9 <meta http-equiv="Cache-Control" content="no-siteapp"/>
10 <!--[if lt IE 9]>
11 <script type="text/javascript" src="lib/html5shiv.js"></script>
12 <script type="text/javascript" src="lib/respond.min.js"></script>
13 <![endif]-->
14 <link href="static/h-ui/css/H-ui.min.css" rel="stylesheet" type="text/css"/>
15 <link href="static/h-ui.admin/css/H-ui.login.css" rel="stylesheet" type="text/css"/>
16 <link href="static/h-ui.admin/css/style.css" rel="stylesheet" type="text/css"/>
17 <link href="lib/Hui-iconfont/1.0.8/iconfont.css" rel="stylesheet" type="text/css"/>
18 <!--[if IE 6]>
19 <script type="text/javascript" src="lib/DD_belatedPNG_0.0.8a-min.js"></script>
20 <script>DD_belatedPNG.fix('*');</script>
21 <![endif]-->
22 <title>后台登錄 - H-ui.admin v3.1</title>
23 <meta name="keywords" content="H-ui.admin v3.1,H-ui網站后台模版,后台模版下載,后台管理系統模版,HTML后台模版下載">
24 <meta name="description" content="H-ui.admin v3.1,是一款由國人開發的輕量級扁平化網站后台模板,完全免費開源的網站后台管理系統模版,適合中小型CMS后台系統。">
25 </head>
26 <body>
27 <input type="hidden" id="TenantId" name="TenantId" value=""/>
28 <div class="header"></div>
29 <div class="loginWraper">
30 <div id="loginform" class="loginBox">
31 <form class="form form-horizontal">
32 <div class="row cl">
33 <label class="form-label col-xs-3"><i class="Hui-iconfont"></i></label>
34 <div class="formControls col-xs-8">
35 <input id="username" name="username" type="text" placeholder="賬號" class="input-text size-L">
36 </div>
37 </div>
38 <div class="row cl">
39 <label class="form-label col-xs-3"><i class="Hui-iconfont"></i></label>
40 <div class="formControls col-xs-8">
41 <input id="password" name="password" type="password" placeholder="密碼" class="input-text size-L">
42 </div>
43 </div>
44 <div class="row cl">
45 <div class="formControls col-xs-8 col-xs-offset-3">
46 <input id="verify" name="verify" class="input-text size-L" type="text" value=""
47 style="width:150px;">
48 <img style="width: 100px;height: 40px;padding: 0px;vertical-align:middle" id="verifycode"
49 src="/api/verify/" onclick="get_verify()"> <a href="javascript:;" onclick="get_verify()">看不清,換一張</a></div>
50 </div>
51 <div class="row cl">
52 <div>
53 <h5 class="formControls col-xs-8 col-xs-offset-3"><span id="msg" style="color:#F00"></span></h5>
54 </div>
55 </div>
56 <div class="row cl">
57 <div class="col-xs-8 col-xs-offset-3">
58 <input type="button" class="btn btn-success size-L" onclick="submit1()"
59 value=" 登 錄 ">
60 </div>
61 </div>
62 </form>
63 </div>
64 </div>
65 <div class="footer">Copyright 你的公司名稱 by H-ui.admin v3.1</div>
66 <script type="text/javascript" src="lib/jquery/1.9.1/jquery.min.js"></script>
67 <script type="text/javascript" src="static/h-ui/js/H-ui.min.js"></script>
68 <script>
69 function submit1() {
70 if ($("#username").val().trim().length == '') {
71 $("#msg").html('').append('請輸入用戶名');
72 }
73 else if ($("#password").val().trim().length == '') {
74 $("#msg").html('').append('請輸入登錄密碼');
75 }
76 else if ($("#verify").val().trim().length != 4) {
77 $("#msg").html('').append('請輸入4位圖形驗證碼');
78 } else {
79 username = $("#username").val();
80 password = $("#password").val();
81 verify = $("#verify").val();
82 $.ajax({
83 type: 'POST',
84 url: "/api/login/",
85 data: {'_method': 'put', 'username': username, 'password': password, 'verify': verify},
86 dataType: 'json',
87 success: function (data) {
88 if(data && data.state>-1){
89 $(location).prop('href', 'main.html');
90 }
91 else{
92 $("#msg").html('').append(data.msg);
93 get_verify();
94 }
95 },
96 error: function(data){
97 if (data){
98 alert(data.msg);
99 }
100 get_verify();
101 }
102 });
103 }
104 }
105
106 function get_verify() {
107 $("#verifycode").attr("src", "/api/verify/?" + 100 * Math.random());
108 }
109
110 </script>
111 </body>
112 </html>
對前面下載的login.html頁面進行了微調,添加了請求的AJAX代碼。
由於火狐和谷歌運行AJAX不支持PUT、DELETE等提交方式,所以AJAX提交時type類型還是POST方式,在提交參數項里面,需要增加 _method 這個參數,值為put。(由於本系列使用的是RESTful風格,所以雖然有點麻煩,但不影響我們的使用)
html和js我也不進行詳細說明,大家自己看代碼吧,如果大家都要求需要對js寫注釋的,我到時再添加注釋進去。
相關頁面功能都完成了,接下來就是進行運行調試
在瀏覽器中輸入:http://127.0.0.1:81/login.html 然后輸入賬號:admin,密碼:123456,還有驗證碼

點擊登錄,能正常跳轉到http://127.0.0.1:81/main.html 頁面,就表示登錄接口能正常使用了。

大家想要熟悉登錄接口代碼的運行,最好使用debug運行跟蹤一下,看看每一行代碼是怎么運行的,就清楚了。當然如果想要加深理解,最佳方式是照着代碼手打一次,每完成幾行就debug運行一下,看看執行效果。

