架构图
后台搭建
用户类
本篇中很重要的一张User表结构,基本上整个后台所有的信息交互都跟其有关,所以先在这里mark一下:
CREATE TABLE `user` (
`uid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户uid',
`nickname` varchar(100) NOT NULL DEFAULT '' COMMENT '用户名',
`mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号码',
`email` varchar(100) NOT NULL DEFAULT '' COMMENT '邮箱地址',
`sex` tinyint(1) NOT NULL DEFAULT '0' COMMENT '1:男 2:女 0:没填写',
`avatar` varchar(64) NOT NULL DEFAULT '' COMMENT '头像',
`login_name` varchar(20) NOT NULL DEFAULT '' COMMENT '登录用户名',
`login_pwd` varchar(32) NOT NULL DEFAULT '' COMMENT '登录密码',
`login_salt` varchar(32) NOT NULL DEFAULT '' COMMENT '登录密码的随机加密秘钥',
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1:有效 0:无效',
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后一次更新时间',
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '插入时间',
PRIMARY KEY (`uid`),
UNIQUE KEY `login_name` (`login_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表(管理员)';
如果把这个建表语句反迁移后,我们会发现flask项目下的User模型类其实后面的comment还有一些字段default都有缺失,所以我感觉这点django的反迁移能力还是要远比flask好用。
代码书写
本篇中前端代码取部分说明,另外我也不是很懂。
首先在www.py下注册蓝图:
# 后台配置蓝图
from web.controllers.index import route_index
from web.controllers.user.User import route_user
from web.controllers.static import route_static
app.register_blueprint( route_index,url_prefix = "/" )
app.register_blueprint(route_user,url_prefix="/user")
app.register_blueprint(route_static,url_prefix="/static")
然后我们可以直接访问我们的登录页面我们就可以在web/controllers/user/User.py下写后端逻辑:
@route_user.route( "/login",methods=["POST","GET"])
def login():
if request.method == "GET": # 如果是get请求直接返回登录页
return render_template("user/login.html")
resp = {"code":200,"msg":"登录成功","data":{}}
req = request.values
login_name = req["login_name"] if "login_name" in req else ""
login_pwd = req["login_pwd"] if "login_pwd" in req else ""
# 对登录名和登录密码的有效性校验,不论前端是否做校验
if login_name is None or len(login_name) < 1 :
resp["code"] = -1
resp["msg"] = "清输入正确的用户名"
return jsonify(resp)
if login_pwd is None or len(login_pwd) < 1:
resp["code"] = -1
resp["meg"] = "清输入正确的登录密码"
return jsonify(resp)
userinfo = User.query.filter_by(login_name=login_name).first() # 因为登录名是唯一的,所以根据用户名查出的用户信息也是唯一
if not userinfo:
resp["code"] = -1
resp["msg"] = "清输入正确的登录用户名和密码-1"
return jsonify(resp)
# todo 通过数据库中的密码与用户填写信息加密后的密码进行校验,我们将这个函数写进通用模块中的UserService
if userinfo.login_pwd != UserService.genePwd(login_pwd,userinfo.login_salt):
resp["code"] = -1
resp["msg"] = "请输入正确的登录用户名和密码-2"
return jsonify(resp)
response = make_response(json.dumps({'code':200, 'msg': '登录成功'}))
# todo 设置了cookie,那么就能设置统一拦截器,防止客户端没有cookie而能进入后台,同时定义cookie的加密方式geneAuthCode
response.set_cookie(app.config["AUTH_COOKIE_NAME"],"{}#{}".format(UserService.geneAuthCode(userinfo),userinfo.id),60 * 60 * 24 * 120) # 生成cookie形式为 16进制加密字符#uid,保存120天
return response
写完上述代码,我们就能开启我们的项目,然后输入127.0.0.1:5000进入登录页,这里的端口号看之前config下base_settings的配置:
然后我们插入本项目老师给的数据库文件下的1.sql文件,将insert语句拷贝进数据库中运行:
INSERT INTO `user` (`uid`, `nickname`, `mobile`, `email`, `sex`, `avatar`, `login_name`, `login_pwd`, `login_salt`, `status`, `updated_time`, `created_time`) VALUES (1, '编程浪子www.54php.cn', '11012345679', '[email protected]', 1, '', '54php.cn', '816440c40b7a9d55ff9eb7b20760862c', 'cF3JfH5FJfQ8B2Ba', 1, '2017-03-15 14:08:48', '2017-03-15 14:08:48');
我们就根据 登录账号是54php.cn 密码是123456 进行登录操作:
在点击登录框之前,将上面的return response改成return redirect("/index/index.html"),那么就能重定向到后台,但那样没有啥太大意义,这样也会导致cookie没法生效。所以接下来继续完善登录的功能。
如果看了注释,上面留下了两个todo,这就是后端登录还需要完善的地方,第一个是密码和cookie加密,第二个是设置全局拦截器。
登录加密:
import hashlib,base64
class UserService():
@staticmethod
def geneAuthCode(user_info = None ):
m = hashlib.md5()
str = "%s-%s-%s-%s" % (user_info.uid, user_info.login_name, user_info.login_pwd, user_info.login_salt)
m.update(str.encode("utf-8"))
return m.hexdigest()
@staticmethod
def genePwd(pwd,salt):
m = hashlib.md5()
# 对密码先进行base64再使用哈希加密
str = "%s-%s" % (base64.encodebytes(pwd.encode("utf-8")),salt)
m.update(str.encode("utf-8"))
return m.hexdigest()
这个算是hashlib加密的固定格式。
全局拦截器:
@app.before_request()
def before_request():
path = request.path
user_info = check_login()
if not user_info:
return redirect(UrlManager.buildUrl("/user/login"))
return
"""
判断用户是否已经登录
"""
def check_login():
cookies = request.cookies # 从请求中获取cookies
auth_cookies = cookies[app.config["AUTH_COOKIE_NAME"]] if app.config["AUTH_COOKIE_NAME"] in cookies else ""
# app.logger.info(auth_cookies) # 查看日志是否有cookies值,可以利用logger,也可以看页面交互模式下application中的cookies
if auth_cookies is None: # 如果页面中没有cookies
return False
splited_cookie = auth_cookies.split("#") # de0e0f7e2848bcbb9e00fd5458393257#1
if len(splited_cookie) != 2: # 不是标准的cookies
return False
try:
user_info = User.query.filter_by(uid=splited_cookie[1]).first() # 查询能否和数据库中cookies对应
except Exception as e:
return False # 查不到接收异常为False
if user_info is None: # 有效性检验,但我感觉没必要,应该会有值,因为数据表设置的是非空,不过显得更加严谨
return False
if splited_cookie[0] != UserService.geneAuthCode( user_info ): # 如果得到的cookies值和通过我们对数据库中值加密过后的不一样,则是伪造
return False
return True
将此文件也注册进www.py蓝图文件中,我们就会发现一个错误:
原因是在我们的拦截器中,我们将所有页面都进行了拦截,包括登录页面,但登录页面是产生cookies的地方,我们一方面需要跳到登录页,而登录页没有cookies却被拦截,那么发再多请求,都将被拦截器拦截,所以,这里我们需要对其进行优化:
在base_settings下加入如下地址:
IGNORE_URLS = [ # 登录地址
"^/user/login"
]
IGNORE_CHECK_LOGIN_URLS = [ # 静态文件路径
"^/static",
"^/favicon.ico"
]
然后在拦截器下增加如下代码:
def before_request():
ignore_urls = app.config['IGNORE_URLS']
ignore_check_login_urls = app.config['IGNORE_CHECK_LOGIN_URLS']
path = request.path
pattern = re.compile('%s' % "|".join(ignore_check_login_urls))
if pattern.match(path):
return
pattern = re.compile('%s' % "|".join(ignore_urls))
if pattern.match(path):
return
那么就可以进入登录页面,登录成功:
当然这里还能为登录按钮加上变灰的效果,还有原始的前端是表单提交,我们需要将其改成ajax的形式,这里因为篇幅还有自身对前端的理解程度问题就不再演示了,我想主要集中看一看作者在common.js通用js中写的统一处理请求的函数:
buildUrl:function( path ,params ){ // 链接管理
var url = "" + path;
var _paramUrl = "";
if( params ){
_paramUrl = Object.keys( params ).map( function( k ){
return [ encodeURIComponent( k ),encodeURIComponent( params[ k ] ) ].join("=");
}).join("&");
/*
例子:params = {
a:"b",
c:"d"
};
结果为:"a=b&c=d"
*/
_paramUrl = "?" + _paramUrl; // 参数拼接
}
return url + _paramUrl; // 完整url
}
感觉之前确实没有在意过前端是怎么接收后端传过来的地址与处理方式,这里倒是理解了一些,然后其余的前端代码我觉得都能敲敲,login.js和edit.js等关于后台的都不是很难,相比于这个都比较直白,几个类似的ajax代码。
之后就是我们的主页编辑功能,我们可以修改我们的个人信息以及登录登出,这些也因为篇幅以及代码复用性问题我就不提了,我们需要注意的是这些相应的接口,怎样在前后端不分离的框架上完成联动,即当前用户怎样传递给每个相应的功能里,那么我们就要改写render方法:
在我们的helper.py文件下加上重写render_template过后的ops_render方法:
'''
统一渲染方法
'''
def ops_render( template,context = {} ):
if 'current_user' in g:
context['current_user'] = g.current_user
return render_template( template,**context )
然后将User下所有的接口返回的render改为ops,那么当前用户就连接了所有接口,就不会出现视频中作者当删除一个用户的记录后,登录页面还是能用其登录,以及更新页面后还是有用户的情况。
最后是三个优化:
第一个优化:账号管理问题
每一次登录当账号进行删除时,我们需要再相关的全局拦截器里定义一个status != 1 false的时候,我们就可以控制只要删除了账号,那么网页就会立即下线。
# AuthInterceptor.py
if user_info.status != 1:
return False
# User.py
if user_info.status != 1:
resp['code'] = -1
resp['msg'] = "账号已被禁用,请联系管理员处理~~"
return jsonify(resp)
第二个优化:解决JS加载缓存问题
如果是不想每次更新完前端js代码都去强制在该文件下刷新缓存,那么我们可以通过对引入的js或者bootstrap更改尾部的版本号的方式,另其随时间变化,那我们就能达到自动更新的结果了。
ver = "%s"%( int( time.time() ) )
path = "/static" + path + "?ver=" + ver
而如果是线上版本与测试版本想进行切换,可以定义一个变量控制,代码为:
@staticmethod
def buildStaticUrl(path):
release_version = app.config.get( 'RELEASE_VERSION' )
ver = "%s"%( int( time.time() ) ) if not release_version else release_version
path = "/static" + path + "?ver=" + ver
return UrlManager.buildUrl( path )
优化三:访问记录问题
从request中读取访问内容,并保存json格式于数据库。
class LogService():
@staticmethod
def addAccessLog():
target = AppAccessLog()
target.target_url = request.url
target.referer_url = request.referrer
target.ip = request.remote_addr
target.query_params = json.dumps( request.values.to_dict() )
if 'current_user' in g and g.current_user is not None:
target.uid = g.current_user.uid
target.ua = request.headers.get( "User-Agent" )
target.created_time = getCurrentDate()
db.session.add( target )
db.session.commit( )
return True
同样,错误日志和上面代码一样,然后我们还可以定义一个404页面与error_hander方法。