我们需要编写验证码图片获取接口和登录处理接口,然后在登录页面的HTML上编写AJAX。
一、修改main.py
在进行接口开发之前,还有一个重要的事情要处理,那就是对站点进行初始化,如果不进行初始化,那么独立文件编写的接口将会找不到,要将异常错误写入日志文件也会找不到路径,下面先上代码。
打开main.py文件,改为下面代码
#!/usr/bin/evn python # coding=utf-8 import bottle import sys import os import logging import urllib.parse from bottle import default_app, get, run, request, hook from beaker.middleware import SessionMiddleware # 导入工具函数包 from common import web_helper, log_helper # 导入api代码模块(初始化api文件夹里的各个访问路由,这一句不能删除,删除后将无法访问api文件夹里的各个接口) import api ############################################# # 初始化bottle框架相关参数 ############################################# # 获取当前main.py文件所在服务器的绝对路径 program_path = os.path.split(os.path.realpath(__file__))[0] # 将路径添加到python环境变量中 sys.path.append(program_path) # 让提交数据最大改为2M(如果想上传更多的文件,可以在这里进行修改) bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 * 2 ############################################# # 初始化日志相关参数 ############################################# # 如果日志目录log文件夹不存在,则创建日志目录 if not os.path.exists('log'): os.mkdir('log') # 初始化日志目录路径 log_path = os.path.join(program_path, 'log') # 定义日志输出格式与路径 logging.basicConfig(level=logging.INFO, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s', filename="%s/info.log" % log_path, filemode='a') # 设置session参数 session_opts = { 'session.type': 'file', 'session.cookie_expires': 3600, 'session.data_dir': '/tmp/sessions/simple', 'session.auto': True } @hook('before_request') def validate(): """使用勾子处理接口访问事件""" # 获取当前访问的Url路径 path_info = request.environ.get("PATH_INFO") # 过滤不用做任何操作的路由(即过滤不用进行判断是否登录和记录日志的url) if path_info in ['/favicon.ico', '/', '/api/verify/']: return ### 记录客户端提交的参数 ### # 获取当前访问url路径与ip request_log = 'url:' + path_info + ' ip:' + web_helper.get_ip() try: # 添加json方式提交的参数 if request.json: request_log = request_log + ' params(json):' + urllib.parse.unquote(str(request.json)) except: pass try: # 添加GET方式提交的参数 if request.query_string: request_log = request_log + ' params(get):' + urllib.parse.unquote(str(request.query_string)) # 添加POST方式提交的参数 if request.method == 'POST': request_log = request_log + ' params(post):' + urllib.parse.unquote(str(request.params.__dict__)) # 存储到日志文件中 log_helper.info(request_log) except: pass # 处理ajax提交的put、delete等请求转换为对应的请求路由(由于AJAX不支持RESTful风格提交,所以需要在这里处理一下,对提交方式进行转换) if request.method == 'POST' and request.POST.get('_method'): request.environ['REQUEST_METHOD'] = request.POST.get('_method', '') # 过滤不用进行登录权限判断的路由(登录与退出登录不用检查是否已经登录) url_list = ["/api/login/", "/api/logout/"] if path_info in url_list: pass else: # 已经登录成功的用户session肯定有值,没有值的就是未登录 session = web_helper.get_session() # 获取用户id manager_id = session.get('id', 0) login_name = session.get('login_name', 0) # 判断用户是否登录 if not manager_id or not login_name: web_helper.return_raise(web_helper.return_msg(-404, "您的登录已失效,请重新登录")) # 函数主入口 if __name__ == '__main__': app_argv = SessionMiddleware(default_app(), session_opts) run(app=app_argv, host='0.0.0.0', port=9090, debug=True, reloader=True) else: # 使用uwsgi方式处理python访问时,必须要添加这一句代码,不然无法访问 application = SessionMiddleware(default_app(), session_opts)
因为我们编写的接口文件都放在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/,就可以看到生成的验证码了。
刷新一下页面,会发现这个验证码还会不断变换其中的字符。
四、登陆流程
在编写登录接口前,我们首先要了解登录接口处理的流程是怎么样的
五、登陆文件
在api文件夹中创建login.py文件,代码如下:
#!/usr/bin/evn python # coding=utf-8 from bottle import put from common import web_helper, encrypt_helper, db_helper @put('/api/login/') def post_login(): """用户登陆验证""" ############################################################## # 获取并验证客户端提交的参数 ############################################################## username = web_helper.get_form('username', '帐号') password = web_helper.get_form('password', '密码') verify = web_helper.get_form('verify', '验证码') ip = web_helper.get_ip() ############################################################## # 从session中读取验证码信息 ############################################################## s = web_helper.get_session() verify_code = s.get('verify_code') # 删除session中的验证码(验证码每提交一次就失效) if 'verify_code' in s: del s['verify_code'] s.save() # 判断用户提交的验证码和存储在session中的验证码是否相同 if verify.upper() != verify_code: return web_helper.return_msg(-1, '验证码错误') ############################################################## ### 获取登录用户记录,并进行登录验证 ### ############################################################## sql = """select * from manager where login_name='%s'""" % (username,) # 从数据库中读取用户信息 manager_result = db_helper.read(sql) # 判断用户记录是否存在 if not manager_result: return web_helper.return_msg(-1, '账户不存在') ############################################################## ### 验证用户登录密码与状态 ### ############################################################## # 对客户端提交上来的验证进行md5加密将转为大写(为了密码的保密性,这里进行双重md5加密,加密时从第一次加密后的密串中提取一段字符串出来进行再次加密,提取的串大家可以自由设定) # pwd = encrypt_helper.md5(encrypt_helper.md5(password)[1:30]).upper() # 对客户端提交上来的验证进行md5加密将转为大写(只加密一次) pwd = encrypt_helper.md5(password).upper() # 检查登录密码输入是否正确 if pwd != manager_result[0].get('login_password', ''): return web_helper.return_msg(-1, '密码错误') # 检查该账号虽否禁用了 if manager_result[0].get('is_enable', 0) == 0: return web_helper.return_msg(-1, '账号已被禁用') ############################################################## ### 把用户信息保存到session中 ### ############################################################## manager_id = manager_result[0].get('id', 0) s['id'] = manager_id s['login_name'] = username s.save() ############################################################## ### 更新用户信息到数据库 ### ############################################################## # 更新当前管理员最后登录时间、Ip与登录次数(字段说明,请看数据字典) sql = """update manager set last_login_time=%s, last_login_ip=%s, login_count=login_count+1 where id=%s""" # 组合更新值 vars = ('now()', ip, manager_id,) # 写入数据库 db_helper.write(sql, vars) return web_helper.return_msg(0, '登录成功')
login.py后台登录处理接口代码可以看到,路由我们使用的是@put('/api/login/'),RESTful风格中,post是用于新增记录,put是用于修改或改变服务器数据,登录我理解它肯定不是新增,它是改变用户登录的状态,所以这里使用put方式接收。
六、前端登录页面
前端登录html页面(login.html),代码如下:
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <meta name="renderer" content="webkit|ie-comp|ie-stand"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"/> <meta http-equiv="Cache-Control" content="no-siteapp"/> <!--[if lt IE 9]> <script type="text/javascript" src="lib/html5shiv.js"></script> <script type="text/javascript" src="lib/respond.min.js"></script> <![endif]--> <link href="static/h-ui/css/H-ui.min.css" rel="stylesheet" type="text/css"/> <link href="static/h-ui.admin/css/H-ui.login.css" rel="stylesheet" type="text/css"/> <link href="static/h-ui.admin/css/style.css" rel="stylesheet" type="text/css"/> <link href="lib/Hui-iconfont/1.0.8/iconfont.css" rel="stylesheet" type="text/css"/> <!--[if IE 6]> <script type="text/javascript" src="lib/DD_belatedPNG_0.0.8a-min.js"></script> <script>DD_belatedPNG.fix('*');</script> <![endif]--> <title>后台登录 - H-ui.admin v3.1</title> <meta name="keywords" content="H-ui.admin v3.1,H-ui网站后台模版,后台模版下载,后台管理系统模版,HTML后台模版下载"> <meta name="description" content="H-ui.admin v3.1,是一款由国人开发的轻量级扁平化网站后台模板,完全免费开源的网站后台管理系统模版,适合中小型CMS后台系统。"> </head> <body> <input type="hidden" id="TenantId" name="TenantId" value=""/> <div class="header"></div> <div class="loginWraper"> <div id="loginform" class="loginBox"> <form class="form form-horizontal"> <div class="row cl"> <label class="form-label col-xs-3"><i class="Hui-iconfont"></i></label> <div class="formControls col-xs-8"> <input id="username" name="username" type="text" placeholder="账号" class="input-text size-L"> </div> </div> <div class="row cl"> <label class="form-label col-xs-3"><i class="Hui-iconfont"></i></label> <div class="formControls col-xs-8"> <input id="password" name="password" type="password" placeholder="密码" class="input-text size-L"> </div> </div> <div class="row cl"> <div class="formControls col-xs-8 col-xs-offset-3"> <input id="verify" name="verify" class="input-text size-L" type="text" value="" style="width:150px;"> <img style="width: 100px;height: 40px;padding: 0px;vertical-align:middle" id="verifycode" src="/api/verify/" onclick="get_verify()"> <a href="javascript:;" onclick="get_verify()">看不清,换一张</a></div> </div> <div class="row cl"> <div> <h5 class="formControls col-xs-8 col-xs-offset-3"><span id="msg" style="color:#F00"></span></h5> </div> </div> <div class="row cl"> <div class="col-xs-8 col-xs-offset-3"> <input type="button" class="btn btn-success size-L" onclick="submit1()" value=" 登 录 "> </div> </div> </form> </div> </div> <div class="footer">Copyright 你的公司名称 by H-ui.admin v3.1</div> <script type="text/javascript" src="lib/jquery/1.9.1/jquery.min.js"></script> <script type="text/javascript" src="static/h-ui/js/H-ui.min.js"></script> <script> function submit1() { if ($("#username").val().trim().length == '') { $("#msg").html('').append('请输入用户名'); } else if ($("#password").val().trim().length == '') { $("#msg").html('').append('请输入登录密码'); } else if ($("#verify").val().trim().length != 4) { $("#msg").html('').append('请输入4位图形验证码'); } else { username = $("#username").val(); password = $("#password").val(); verify = $("#verify").val(); $.ajax({ type: 'POST', url: "/api/login/", data: {'_method': 'put', 'username': username, 'password': password, 'verify': verify}, dataType: 'json', success: function (data) { if(data && data.state>-1){ $(location).prop('href', 'main.html'); } else{ $("#msg").html('').append(data.msg); get_verify(); } }, error: function(data){ if (data){ alert(data.msg); } get_verify(); } }); } } function get_verify() { $("#verifycode").attr("src", "/api/verify/?" + 100 * Math.random()); } </script> </body> </html>
由于火狐和谷歌运行AJAX不支持PUT、DELETE等提交方式,所以AJAX提交时type类型还是POST方式,在提交参数项里面,需要增加 _method 这个参数,值为put。(由于本系列使用的是RESTful风格,所以虽然有点麻烦,但不影响我们的使用)
七、测试登陆
在浏览器中输入:http://127.0.0.1:81/login.html 然后输入账号:admin,密码:123456,还有验证码
点击登录,能正常跳转到http://127.0.0.1:81/main.html 页面,就表示登录接口能正常使用了。