1、后台用户登录验证
1、用户登录的操作是post提交方式:
将后台用户提交的form表单信息收集:创建cms/forms.py文件
后台登录用户表单收集文件:forms.py文件
# -*- encoding: utf-8 -*-
"""
@File : forms.py
@Time : 2020/5/11 10:00
@Author : chen
"""
# forms表单信息
from wtforms import Form, StringField, IntegerField
from wtforms.validators import Email, InputRequired, Length
class LoginForm(Form):
email = StringField(validators=[Email(message="请输入正确的邮箱"), InputRequired(message="请输入邮箱")])
password = StringField(validators=[Length(3, 15, message='请输入正确长度的密码')])
remember = IntegerField() # 记住cookie操作 赋值为0或1
forms.py文件收集到的后台登录用户字段信息提交至views.py文件
验证登录用户表单信息是否匹配数据库中的信息:views.py
# -*- encoding: utf-8 -*-
"""
@File : views.py
@Time : 2020/5/11 9:59
@Author : chen
"""
# 蓝图文件:实现模块化应用,应用可以分解成一系列的蓝图 后端的类视图函数写在这个文件
from flask import Blueprint, render_template, views, session # 定义类视图,显示模板文件
from flask import request, redirect, url_for # 页面跳转redirect request请求收集
# 导入form表单 .forms代表同级目录下的forms.py
from .forms import LoginForm
# 导入模型 .models代表同级目录下的models.py
from .models import CMS_User
cms_bp = Blueprint("cms", __name__, url_prefix='/cms/') # URL前缀url_prefix
@cms_bp.route("/") # 后台界面
def index():
return "cms index:后端类视图文件"
# 定义类视图,显示模板文件
class LoginView(views.MethodView):
def get(self):
return render_template("cms/cms_login.html")
# 用户登录操作验证
def post(self):
# 收集表单信息
login_form = LoginForm(request.form)
if login_form.validate():
# 数据库验证
email = login_form.email.data
password = login_form.password.data
remember = login_form.remember.data
# 查询数据库中的用户信息
user = CMS_User.query.filter_by(email=email).first() # 邮箱唯一,用于查询验证用户
if user and user.check_password(password): # 验证用户和密码是否都正确
session['user_id'] = user.id # 查询到用户数据时,保存session的id到浏览器
if remember: # 如果用户点击了remember选择,在浏览器中进行数据持久化
session.permanent = True # 数据持久化,默认31天,需要设置session_key在config.py中
# 登录成功,跳转到后台首页
return redirect(url_for('cms.index')) # 在蓝图中必须加cms 跳转到index方法
else:
return "邮箱或密码错误" # 登录出错,返回结果
else:
print(login_form.errors)
return "表单验证错误"
# 添加登录路由
cms_bp.add_url_rule("/login/", view_func=LoginView.as_view('login')) # view_func 命名操作名字,"/login/"路由地址
views.py文件中有一个密码验证的方法check_password(password),需要在模型文件models.py中进行添加;
验证用户信息之后,包含session信息的加密方式,需要设置session_key在config.py中。
模型文件:models.py
# -*- encoding: utf-8 -*-
"""
@File : models.py
@Time : 2020/5/11 10:00
@Author : chen
"""
# 定义后端用户模型
from exts import db
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash # 导入密码加密,解密方法的库
class CMS_User(db.Model):
__tablename__ = 'cms_user'
id = db.Column(db.Integer, primary_key=True, autoincrement=True) # 主键 自增
username = db.Column(db.String(150), nullable=False) # 非空
# password = db.Column(db.String(150), nullable=False)
_password = db.Column(db.String(150), nullable=False) # 密码加密操作修改字段
email = db.Column(db.String(50), nullable=False, unique=True) # 非空、唯一
join_time = db.Column(db.DateTime, default=datetime.now) # 默认当前时间
# 修改密码加密操作中的字段,在manage.py映射数据库时候,使用字段还是保持相同
def __init__(self, username, password, email):
self.username = username
self.password = password # 调用该方法 返回下面的self._password数值,
self.email = email
# 密码加密操作
@property
def password(self): # 密码取值
return self._password
@password.setter # 密码加密
def password(self, raw_password):
self._password = generate_password_hash(raw_password)
# 用于验证后台登录密码是否和数据库一致,raw_password是后台登录输入的密码
def check_password(self, raw_password):
result = check_password_hash(self.password, raw_password) # 相当于用相同的hash加密算法加密raw_password,检测与数据库中是否一致
return result
配置文件:config.py
import os # 导入随机字符串用于加密session
SECRET_KEY = os.urandom(15) # 产生随机15位字符串加密
2、错误登录信息渲染到前端界面
在后台登录的过程中,用户信息与数据库中数据不匹配的时候,产生的错误信息需要给用户看到错误的种类,此时就需要将错误信息渲染到后台登录的前端界面上,即cms_login.html页面。
views.py文件中将错误信息返回
# -*- encoding: utf-8 -*-
"""
@File : views.py
@Time : 2020/5/11 9:59
@Author : chen
"""
# 蓝图文件:实现模块化应用,应用可以分解成一系列的蓝图 后端的类视图函数写在这个文件
from flask import Blueprint, render_template, views, session # 定义类视图,显示模板文件
from flask import request, redirect, url_for # 页面跳转redirect request请求收集
# 导入form表单 .forms代表同级目录下的forms.py
from .forms import LoginForm
# 导入模型 .models代表同级目录下的models.py
from .models import CMS_User
cms_bp = Blueprint("cms", __name__, url_prefix='/cms/') # URL前缀url_prefix
@cms_bp.route("/") # 后台界面
def index():
return "cms index:后端类视图文件"
# 定义类视图,显示模板文件
class LoginView(views.MethodView):
def get(self, message=None): # message=None时候不传输信息到cms_login.html页面
return render_template("cms/cms_login.html", message=message) # 针对post方法中同样要返回到cms_login.html页面进行代码简化
# 用户登录操作验证
def post(self):
# 收集表单信息
login_form = LoginForm(request.form)
if login_form.validate():
# 数据库验证
email = login_form.email.data
password = login_form.password.data
remember = login_form.remember.data
# 查询数据库中的用户信息
user = CMS_User.query.filter_by(email=email).first() # 邮箱唯一,用于查询验证用户
if user and user.check_password(password): # 验证用户和密码是否都正确
session['user_id'] = user.id # 查询到用户数据时,保存session的id到浏览器
if remember: # 如果用户点击了remember选择,在浏览器中进行数据持久化
session.permanent = True # 数据持久化,默认31天,需要设置session_key在config.py中
# 登录成功,跳转到后台首页
return redirect(url_for('cms.index')) # 在蓝图中必须加cms 跳转到index方法
else:
# return "邮箱或密码错误" # 登录出错,返回结果
# return render_template("cms/cms_login.html", message="邮箱或密码错误") # 登录出错,返回结果渲染到cms_login.html页面
return self.get(message="邮箱或密码错误") # 传参到get方法中,多加一个传输错误信息的参数到方法中
else:
print(login_form.errors) # forms.py中的错误信息 字典类型数据
print(login_form.errors.popitem()) # forms.py中的错误信息 元祖类型数据
# return "表单验证错误" # 错误信息需要渲染到cms_login.html页面
return self.get(message=login_form.errors.popitem()[1][0]) # 字典类型数据信息提取
# 添加登录路由
cms_bp.add_url_rule("/login/", view_func=LoginView.as_view('login')) # view_func 命名操作名字,"/login/"路由地址
其中要注意的是get()方法的修改和login_form.errors表单错误信息的提取。
渲染页面cms_login.html进行传参message
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
<meta name="description" content="">
<meta name="author" content="">
<!-- <link rel="icon" href="../../favicon.ico">-->
<title>CMS后台登录界面</title>
<!-- Bootstrap core CSS -->
<link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<!-- Just for debugging purposes. Don't actually copy these 2 lines! -->
<!--[if lt IE 9]><script src="../../assets/js/ie8-responsive-file-warning.js"></script><![endif]-->
<script src="../../assets/js/ie-emulation-modes-warning.js"></script>
<!-- Custom styles for this template -->
<!-- <link href="signin.css" rel="stylesheet">-->
<!-- 这里引用自己的css模板文件 模板中引用静态资源文件使用url_for,路径filename='cms/css/signin.css')需要相对路径中的绝对路径-->
<link href="{{ url_for('static', filename='cms/css/signin.css') }}" rel="stylesheet">
</head>
<body>
<div class="container">
<!-- 添加登录方法method="post" -->
<form class="form-signin" method="post">
<h2 class="form-signin-heading">请登录</h2>
<label for="inputEmail" class="sr-only">邮箱</label>
<!-- 表单提交根据name="email"来接收后端数据 -->
<input type="email" id="inputEmail" name="email" class="form-control" placeholder="邮箱地址" required autofocus>
<label for="inputPassword" class="sr-only">密码</label>
<!-- 表单提交根据name="password"来接收后端数据 -->
<input type="password" id="inputPassword" name="password" class="form-control" placeholder="填写密码" required>
<div class="checkbox">
<label>
<!-- 表单提交根据name="remember"来接收后端数据 -->
<input type="checkbox" value="remember-me" name="remember"> 记住我
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">立即登录</button>
</form>
<!-- views.py中的表单验证输出的错误参数message进行渲染到前端界面 先判断message是否有信息,没有就不进行渲染 -->
{% if message %}
<!-- style="text-align:center" class="text-danger" 居中、红色字体样式 -->
<p style="text-align:center" class="text-danger"> {{ message }}</p>
{% endif %}
</div> <!-- /container -->
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<script src="../../assets/js/ie10-viewport-bug-workaround.js"></script>
</body>
</html>
这里需要注意的是:先判断message是否传有信息,没有就不进行渲染到界面中,运用到了if知识点。
3、钩子函数和装饰器
为了实现判断当前界面是否是登录界面,不是就将url重定向到登录界面这一功能,有两种方法实现:钩子函数、装饰器;分别进行介绍。
钩子函数
钩子函数中有函数方法before_first_request:处理第一次请求之前执行
用于实现判断当前界面是否是登录界面,不是就将url重定向到登录界面。
创建cms/hooks.py文件用于存放钩子函数
# -*- encoding: utf-8 -*-
"""
@File : hooks.py
@Time : 2020/5/13 9:36
@Author : chen
"""
from flask import request, session, url_for,redirect
from .views import cms_bp
# 钩子函数 ,所有操作前执行该方法,判断当前界面是否是登录界面,不是就将url重定向到登录界面
@cms_bp.before_request
def before_request():
print(request.path) # 输出的是网页url的后缀,即/cms/login/
if not request.path.endswith(url_for('cms.login')): # 判断当前所在url是否是/cms/login/,不是代表不在后台登录界面
user_id = session.get('user_id') # 登陆之后,获取登录时候记录的session中的user_id
if not user_id: # 若没有user_id,说明登录不成功
return redirect(url_for('cms.login')) # 重定向到后台登录界面
装饰器
装饰器也可以实现上面的功能,但是比较复杂难懂,后面创建的路由函数也都要再加上这个装饰器,比较麻烦,不建议使用。
创建cms/decorators.py文件用于存放装饰器
# -*- encoding: utf-8 -*-
"""
@File : decorators.py
@Time : 2020/5/12 22:38
@Author : chen
"""
# 装饰器方法实现另一种判定后台用户当前界面是否是登录界面,不是就重定向到登录界面
from flask import session
from flask import redirect, url_for
def login_required(func):
def inner(*args, **kwargs): # 内层函数
if 'user_id' in session:
return func(*args, **kwargs)
else:
return redirect(url_for("cms.login"))
return inner
需要注意的是,需要将上面的两种方法选择一个导入到cms/views.py文件中使用验证(推荐使用钩子函数,简单方便),这里注意导入的方式:
cms/views.py文件导入钩子函数
# -*- encoding: utf-8 -*-
"""
@File : views.py
@Time : 2020/5/11 9:59
@Author : chen
"""
# 蓝图文件:实现模块化应用,应用可以分解成一系列的蓝图 后端的类视图函数写在这个文件
from flask import Blueprint, render_template, views, session # 定义类视图,显示模板文件
from flask import request, redirect, url_for # 页面跳转redirect request请求收集
# 导入form表单 .forms代表同级目录下的forms.py
from .forms import LoginForm
# 导入模型 .models代表同级目录下的models.py
from .models import CMS_User
# 导入装饰器:判断当前界面是否是登录界面,不是就将url重定向到登录界面
from .decorators import login_required
cms_bp = Blueprint("cms", __name__, url_prefix='/cms/') # URL前缀url_prefix
# 钩子函数是在cms_bp创建之后才创建的,顺序在cms_bp创建之后
from .hooks import before_request
@cms_bp.route("/") # 后台界面
# @login_required # 装饰器判定当前界面是否是登录界面,但是需要每个路由函数都要加该装饰器,比较麻烦,推荐使用钩子函数
def index():
return "cms index:后端类视图文件"
# 定义类视图,显示模板文件 用户登录功能实现
class LoginView(views.MethodView):
def get(self, message=None): # message=None时候不传输信息到cms_login.html页面
return render_template("cms/cms_login.html", message=message) # 针对post方法中同样要返回到cms_login.html页面进行代码简化
# 用户登录操作验证
def post(self):
# 收集表单信息
login_form = LoginForm(request.form)
if login_form.validate():
# 数据库验证
email = login_form.email.data
password = login_form.password.data
remember = login_form.remember.data
# 查询数据库中的用户信息
user = CMS_User.query.filter_by(email=email).first() # 邮箱唯一,用于查询验证用户
if user and user.check_password(password): # 验证用户和密码是否都正确
session['user_id'] = user.id # 查询到用户数据时,保存session的id到浏览器
if remember: # 如果用户点击了remember选择,在浏览器中进行数据持久化
session.permanent = True # 数据持久化,默认31天,需要设置session_key在config.py中
# 登录成功,跳转到后台首页
return redirect(url_for('cms.index')) # 在蓝图中必须加cms 跳转到index方法
else:
# return "邮箱或密码错误" # 登录出错,返回结果
# return render_template("cms/cms_login.html", message="邮箱或密码错误") # 登录出错,返回结果渲染到cms_login.html页面
return self.get(message="邮箱或密码错误") # 传参到get方法中,多加一个传输错误信息的参数到方法中
else:
print(login_form.errors) # forms.py中的错误信息 字典类型数据
print(login_form.errors.popitem()) # forms.py中的错误信息 元祖类型数据
# return "表单验证错误" # 错误信息需要渲染到cms_login.html页面
return self.get(message=login_form.errors.popitem()[1][0]) # 字典类型数据信息提取
# 添加登录路由
cms_bp.add_url_rule("/login/", view_func=LoginView.as_view('login')) # view_func 命名操作名字,"/login/"路由地址
4、CSRF验证保护
CSRF验证保护是属于WEB安全中的知识点:需要在form表单进行post提交的时候,我们添加一个CSRF的token,用于确认访问者的身份确认,防止爬虫和黑客。
程序主文件引入CSRF保护:
项目主文件,启动入口bbs.py
# -*- encoding: utf-8 -*-
"""
@File : bbs.py
@Time : 2020/5/11 9:46
@Author : chen
"""
# 项目主文件,启动入口
# 前台 front 管理前端界面的逻辑
# 后台 cms 管理后端的操作
# 公有的文件 common
from flask import Flask
import config # 配置文件库
from exts import db # 第三方库导入db
from apps.cms.views import cms_bp # 导入后端蓝图文件
from apps.front.views import front_bp # 导入前端蓝图文件
from flask_wtf import CSRFProtect # CSRF表单保护验证
app = Flask(__name__)
CSRFProtect(app) # CSRF保护app
app.config.from_object(config) # 添加配置
db.init_app(app) # 绑定app
app.register_blueprint(cms_bp) # 后端蓝图文件注册
app.register_blueprint(front_bp) # 前端蓝图文件注册
if __name__ == '__main__':
app.run(debug=True, port=9999)
此时还需要在渲染的html文件中添加csrf_token信息用于验证:
后台显示界面cms_login.html文件
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
<meta name="description" content="">
<meta name="author" content="">
<!-- <link rel="icon" href="../../favicon.ico">-->
<title>CMS后台登录界面</title>
<!-- Bootstrap core CSS -->
<link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<!-- 这里引用自己的css模板文件 模板中引用静态资源文件使用url_for,路径filename='cms/css/signin.css')需要相对路径中的绝对路径-->
<link href="{{ url_for('static', filename='cms/css/signin.css') }}" rel="stylesheet">
</head>
<body>
<div class="container">
<!-- 添加登录方法method="post" -->
<form class="form-signin" method="post">
<!-- CSRF保护验证 type="hidden"页面隐藏这个信息,只用于验证保护 方法调用csrf_token() -->
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<h2 class="form-signin-heading">请登录</h2>
<label for="inputEmail" class="sr-only">邮箱</label>
<!-- 表单提交根据name="email"来接收后端数据 -->
<input type="email" id="inputEmail" name="email" class="form-control" placeholder="邮箱地址" required autofocus>
<label for="inputPassword" class="sr-only">密码</label>
<!-- 表单提交根据name="password"来接收后端数据 -->
<input type="password" id="inputPassword" name="password" class="form-control" placeholder="填写密码" required>
<div class="checkbox">
<label>
<!-- 表单提交根据name="remember"来接收后端数据 -->
<input type="checkbox" value="remember-me" name="remember"> 记住我
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">立即登录</button>
</form>
<!-- views.py中的表单验证输出的错误参数message进行渲染到前端界面 先判断message是否有信息,没有就不进行渲染 -->
{% if message %}
<!-- style="text-align:center" class="text-danger" 居中、红色字体样式 -->
<p style="text-align:center" class="text-danger"> {{ message }}</p>
{% endif %}
</div> <!-- /container -->
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<script src="../../assets/js/ie10-viewport-bug-workaround.js"></script>
</body>
</html>
5、用户名渲染和注销功能
这里需要添加几个模板文件:
静态文件:static/cms/css/cms_base.css文件
/*
* Base structure
*/
/* Move down content because we have a fixed navbar that is 50px tall */
body {
padding-top: 50px;
overflow: hidden;
}
/*
* Global add-ons
*/
.sub-header {
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
/*
* Top navigation
* Hide default border to remove 1px line.
*/
.navbar-fixed-top {
border: 0;
}
/*
* Sidebar
*/
/* Hide for mobile, show later */
.sidebar {
display: none;
}
@media (min-width: 768px) {
.sidebar {
position: fixed;
top: 51px;
bottom: 0;
left: 0;
z-index: 1000;
display: block;
padding: 20px;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
background-color: #363a47;
border-right: 1px solid #eee;
margin-top: -1px;
}
}
.nav-sidebar{
padding: 5px 0;
margin-left: -20px;
margin-right: -20px;
}
.nav-sidebar > li{
background: #494f60;
border-bottom: 1px solid #363a47;
border-top: 1px solid #666;
line-height: 35px;
}
.nav-sidebar > li > a {
background: #494f60;
color: #9b9fb1;
margin-left: 25px;
display: block;
}
.nav-sidebar > li a span{
float: right;
width: 10px;
height:10px;
border-style: solid;
border-color: #9b9fb1 #9b9fb1 transparent transparent;
border-width: 1px;
transform: rotate(45deg);
position: relative;
top: 10px;
margin-right: 10px;
}
.nav-sidebar > li > a:hover{
color: #fff;
background: #494f60;
text-decoration: none;
}
.nav-sidebar > li > .subnav{
display: none;
}
.nav-sidebar > li.unfold{
background: #494f60;
}
.nav-sidebar > li.unfold > .subnav{
display: block;
}
.nav-sidebar > li.unfold > a{
color: #db4055;
}
.nav-sidebar > li.unfold > a span{
transform: rotate(135deg);
top: 5px;
border-color: #db4055 #db4055 transparent transparent;
}
.subnav{
padding-left: 10px;
padding-right: 10px;
background: #363a47;
overflow: hidden;
}
.subnav li{
overflow: hidden;
margin-top: 10px;
line-height: 25px;
height: 25px;
}
.subnav li.active{
background: #db4055;
}
.subnav li a{
/*display: block;*/
color: #9b9fb1;
padding-left: 30px;
height:25px;
line-height: 25px;
}
.subnav li a:hover{
color: #fff;
}
.nav-group{
margin-top: 10px;
}
.main {
padding: 20px;
}
@media (min-width: 768px) {
.main {
padding-right: 40px;
padding-left: 40px;
}
}
.main .page-header {
margin-top: 0;
}
/*
* Placeholder dashboard ideas
*/
.placeholders {
margin-bottom: 30px;
text-align: center;
}
.placeholders h4 {
margin-bottom: 0;
}
.placeholder {
margin-bottom: 20px;
}
.placeholder img {
display: inline-block;
border-radius: 50%;
}
.main_content{
margin-top: 20px;
}
.top-group{
padding: 5px 10px;
border-radius: 2px;
background: #ecedf0;
overflow: hidden;
}
静态文件:static/cms/css/sigin.css文件
body {
padding-top: 40px;
padding-bottom: 40px;
background-color: #eee;
}
.form-signin {
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
.form-signin .form-signin-heading,
.form-signin .checkbox {
margin-bottom: 10px;
}
.form-signin .checkbox {
font-weight: normal;
}
.form-signin .form-control {
position: relative;
height: auto;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
静态文件:static/cms/js/cms_base.js文件
/**
* Created by Administrator on 2016/12/17.
*/
$(function () {
$('.nav-sidebar>li>a').click(function (event) {
var that = $(this);
if(that.children('a').attr('href') == '#'){
event.preventDefault();
}
if(that.parent().hasClass('unfold')){
that.parent().removeClass('unfold');
}else{
that.parent().addClass('unfold').siblings().removeClass('unfold');
}
console.log('coming....');
});
$('.nav-sidebar a').mouseleave(function () {
$(this).css('text-decoration','none');
});
});
$(function () {
var url = window.location.href;
if(url.indexOf('profile') >= 0){
var profileLi = $('.profile-li');
profileLi.addClass('unfold').siblings().removeClass('unfold');
profileLi.children('.subnav').children().eq(0).addClass('active').siblings().removeClass('active');
} else if(url.indexOf('resetpwd') >= 0){
var profileLi = $('.profile-li');
profileLi.addClass('unfold').siblings().removeClass('unfold');
profileLi.children('.subnav').children().eq(1).addClass('active').siblings().removeClass('active');
} else if(url.indexOf('resetemail') >= 0){
var profileLi = $('.profile-li');
profileLi.addClass('unfold').siblings().removeClass('unfold');
profileLi.children('.subnav').children().eq(2).addClass('active').siblings().removeClass('active');
} else if(url.indexOf('posts') >= 0){
var postManageLi = $('.post-manage');
postManageLi.addClass('unfold').siblings().removeClass('unfold');
}else if(url.indexOf('boards') >= 0){
var boardManageLi = $('.board-manage');
boardManageLi.addClass('unfold').siblings().removeClass('unfold');
}else if(url.indexOf('permissions') >= 0){
var permissionManageLi = $('.permission-manage');
permissionManageLi.addClass('unfold').siblings().removeClass('unfold');
}else if(url.indexOf('roles') >= 0){
var roleManageLi = $('.role-manage');
roleManageLi.addClass('unfold').siblings().removeClass('unfold');
}else if(url.indexOf('users') >= 0){
var userManageLi = $('.user-manage');
userManageLi.addClass('unfold').siblings().removeClass('unfold');
}else if(url.indexOf('cmsuser_manage') >= 0){
var cmsuserManageLi = $('.cmsuser-manage');
cmsuserManageLi.addClass('unfold').siblings().removeClass('unfold');
}else if(url.indexOf('cmsrole_manage') >= 0){
var cmsroleManageLi = $('.cmsrole-manage');
cmsroleManageLi.addClass('unfold').siblings().removeClass('unfold');
}else if(url.indexOf('comments') >= 0) {
var commentsManageLi = $('.comments-manage');
commentsManageLi.addClass('unfold').siblings().removeClass('unfold');
}
});
静态文件:static/cms/js/resetpwd.js文件
$(function () {
$("#submit").click(function (event) {
// event.preventDefault
// 是阻止按钮默认的提交表单的事件
event.preventDefault();
var oldpwdE = $("input[name=oldpwd]");
var newpwdE = $("input[name=newpwd]");
var newpwd2E = $("input[name=newpwd2]");
var oldpwd = oldpwdE.val();
var newpwd = newpwdE.val();
var newpwd2 = newpwd2E.val();
// 1. 要在模版的meta标签中渲染一个csrf-token
// 2. 在ajax请求的头部中设置X-CSRFtoken
var lgajax = {
'get':function(args) {
args['method'] = 'get';
this.ajax(args);
},
'post':function(args) {
args['method'] = 'post';
this.ajax(args);
},
'ajax':function(args) {
// 设置csrftoken
this._ajaxSetup();
$.ajax(args);
},
'_ajaxSetup': function() {
$.ajaxSetup({
'beforeSend':function(xhr,settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
// var csrftoken = $('meta[name=csrf-token]').attr('content');
var csrftoken = $('input[name=csrf-token]').attr('value');
xhr.setRequestHeader("X-CSRFToken", csrftoken)
}
}
});
}
};
lgajax.post({
'url': '/cms/resetpwd/',
'data': {
'oldpwd': oldpwd,
'newpwd': newpwd,
'newpwd2': newpwd2
},
'success': function (data) {
console.log(data);
},
'fail': function (error) {
console.log(error);
}
});
});
});
模板文件:templates/cms/cms_index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>标题</title>
<script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
<link href="http://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<script src="http://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<!-- 关联本地的cms_base.css样式 后台管理界面CMS的样式 -->
<link rel="stylesheet" href="{{ url_for('static', filename='cms/css/cms_base.css') }}">
<!-- 关联本地的cms_base.js样式 后台管理界面CMS的样式 -->
<script src="{{ url_for('static', filename='cms/js/cms_base.js') }}"></script>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">论坛CMS管理系统</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<!-- 从数据库中调用用户名,g对象全局调用g.cms_user -->
<li><a href="#">{{ g.cms_user }}<span>[超级管理员]</span></a></li>
<!-- 用户注销,关联到views.py中的@cms_bp.route("/logout/")路由,重定向到该路由 -->
<li><a href="{{ url_for('cms.logout') }}">注销</a></li>
</ul>
<form class="navbar-form navbar-right">
<input type="text" class="form-control" placeholder="查找...">
</form>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav-sidebar">
<li class="unfold"><a href="#">首页</a></li>
<li class="profile-li">
<a href="#">个人中心<span></span></a>
<ul class="subnav">
<li><a href="#">个人信息</a></li>
<li><a href="#">修改密码</a></li>
<li><a href="#">修改邮箱</a></li>
</ul>
</li>
<li class="nav-group post-manage"><a href="#">帖子管理</a></li>
<li class="comments-manage"><a href="#">评论管理</a></li>
<li class="board-manage"><a href="#">板块管理</a></li>
<li class="nav-group user-manage"><a href="#">用户管理</a></li>
<li class="role-manage"><a href="#">组管理</a></li>
<li class="nav-group cmsuser-manage"><a href="#">CMS用户管理</a></li>
<li class="cmsrole-manage"><a href="#">CMS组管理</a></li>
</ul>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1>BBS论坛</h1>
<div class="main_content">
欢迎来到BBS论坛
</div>
</div>
</div>
</div>
</body>
</html>
还需要对蓝图文件apps/cms/views.py进行修改:
- 1、session中的用户信息需要传递到apps/cms/hooks.py中进行判断;
- 2、对用户的注销操作进行函数编写,同时传递关联到templates/cms/cms_index.html中的注销属性;
# -*- encoding: utf-8 -*-
# -*- encoding: utf-8 -*-
"""
@File : views.py
@Time : 2020/5/11 9:59
@Author : chen
"""
# 蓝图文件:实现模块化应用,应用可以分解成一系列的蓝图 后端的类视图函数写在这个文件
from flask import Blueprint, render_template, views, session # 定义类视图,显示模板文件
from flask import request, redirect, url_for # 页面跳转redirect request请求收集
# 导入form表单 .forms代表同级目录下的forms.py
from .forms import LoginForm
# 导入模型 .models代表同级目录下的models.py
from .models import CMS_User
# 导入装饰器:判断当前界面是否是登录界面,不是就将url重定向到登录界面
from .decorators import login_required
cms_bp = Blueprint("cms", __name__, url_prefix='/cms/') # URL前缀url_prefix
# 钩子函数是在cms_bp创建之后才创建的,顺序在cms_bp创建之后
from .hooks import before_request
@cms_bp.route("/") # 后台界面
# @login_required # 装饰器判定当前界面是否是登录界面,但是需要每个路由函数都要加该装饰器,比较麻烦,推荐使用钩子函数
def index():
# return "cms index:后端类视图文件"
return render_template('cms/cms_index.html') # 登陆之后进入CMS后台管理界面,路径写全cms/cms_index.html
# 用户注销登录
@cms_bp.route("/logout/") # 需要关联到cms/cms_index.html中的注销属性
def logout():
# session清除user_id
del session['user_id']
# 重定向到登录界面
return redirect(url_for('cms.login')) # 重定向(redirec)为把url变为重定向的url
# 定义个人中心的路由
@cms_bp.route("/profile/")
def profile():
return render_template("cms/cms_profile.html") # 模板渲染(render_template)则不会改变url,模板渲染是用模板来渲染请求的url
# 定义类视图,显示模板文件 用户登录功能实现
class LoginView(views.MethodView):
def get(self, message=None): # message=None时候不传输信息到cms_login.html页面
return render_template("cms/cms_login.html", message=message) # 针对post方法中同样要返回到cms_login.html页面进行代码简化
# 用户登录操作验证
def post(self):
# 收集表单信息
login_form = LoginForm(request.form)
if login_form.validate():
# 数据库验证
email = login_form.email.data
password = login_form.password.data
remember = login_form.remember.data
# 查询数据库中的用户信息
user = CMS_User.query.filter_by(email=email).first() # 邮箱唯一,用于查询验证用户
if user and user.check_password(password): # 验证用户和密码是否都正确
session['user_id'] = user.id # 查询到用户数据时,保存session的id到浏览器
# session['user_name'] = user.username # 将数据库中的user.username保存到session中,在hooks.py中判断
# session['user_email'] = user.email # 将数据库中的email保存到session中,方便html调用信息
# session['user_join_time'] = user.join_time # 将数据库中的join_time保存到session中,方便html调用信息
if remember: # 如果用户点击了remember选择,在浏览器中进行数据持久化
session.permanent = True # 数据持久化,默认31天,需要设置session_key在config.py中
# 登录成功,跳转到后台首页
return redirect(url_for('cms.index')) # 在蓝图中必须加cms 跳转到index方法
else:
# return "邮箱或密码错误" # 登录出错,返回结果
# return render_template("cms/cms_login.html", message="邮箱或密码错误") # 登录出错,返回结果渲染到cms_login.html页面
return self.get(message="邮箱或密码错误") # 传参到get方法中,多加一个传输错误信息的参数到方法中
else:
print(login_form.errors) # forms.py中的错误信息 字典类型数据
print(login_form.errors.popitem()) # forms.py中的错误信息 元祖类型数据
# return "表单验证错误" # 错误信息需要渲染到cms_login.html页面
return self.get(message=login_form.errors.popitem()[1][0]) # 字典类型数据信息提取
# 添加登录路由
cms_bp.add_url_rule("/login/", view_func=LoginView.as_view('login')) # view_func 命名操作名字,"/login/"路由地址
session中的用户信息需要传递到apps/cms/hooks.py中进行判断:
修改apps/cms/hooks.py文件
# -*- encoding: utf-8 -*-
"""
@File : hooks.py
@Time : 2020/5/13 9:36
@Author : chen
"""
from flask import request, session, url_for, redirect, g # g对象全局变量gloabl,方便调用
from .views import cms_bp
from .models import CMS_User
# 钩子函数 ,所有操作前执行该方法,判断当前界面是否是登录界面,不是就将url重定向到登录界面
@cms_bp.before_request
def before_request():
print(request.path) # 输出的是网页url的后缀,即/cms/login/
if not request.path.endswith(url_for('cms.login')): # 判断当前所在url是否是/cms/login/,不是代表不在后台登录界面
user_id = session.get('user_id') # 登陆之后,获取登录时候记录的session中的user_id
if not user_id: # 若没有user_id,说明登录不成功
return redirect(url_for('cms.login')) # 重定向到后台登录界面
# 判断user_id是否登陆过,登录之后就返回用户名到CMS后台管理系统
if 'user_id' in session:
user_id = session.get('user_id') # 调用session中user_id
user = CMS_User.query.get(user_id) # 通过user_id查询到用户对象,方便前端界面调用对象中的字段属性
if user:
g.cms_user = user # 赋值给g对象,全局变量g.cms_user用于渲染到后台管理界面cms_index.html
# 上面的代码相对于下面的来说比较简单,下面的是将对象中的字段属性单独来调用并修改为全局变量,上面只是将完整的一个对象变成全局变量
'''
# 判断user_id是否登陆过,登录之后就返回用户名到CMS后台管理系统
if 'user_id' in session: # user_id在session中,说明cms用户已经登录了
user_name = session.get('user_name') # 从session中调用user_username
user_email = session.get('user_email') # 从session中调用user_email,用于设置为全局变量,渲染到cms_profile.html中
user_join_time = session.get('user_join_time') # 从session中调用user_join_time,用于设置为全局变量,渲染到cms_profile.html中
if user_name:
g.cms_user = user_name # 赋值给g对象,全局变量g.cms_user用于渲染到后台管理界面cms_index.html
g.cms_email = user_email # 设置为全局变量,渲染到cms_profile.html中
g.cms_join_time = user_join_time # 设置为全局变量,渲染到cms_profile.html中
'''
添加新的用户用于测试数据显示是否正常:
7、模板继承
创建新的模板文件templates/cms/cms_base.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}
{% endblock %}</title>
<script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
<link href="http://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<script src="http://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<!-- 关联本地的cms_base.css样式 后台管理界面CMS的样式 -->
<link rel="stylesheet" href="{{ url_for('static', filename='cms/css/cms_base.css') }}">
<!-- 关联本地的cms_base.js样式 后台管理界面CMS的样式 -->
<script src="{{ url_for('static', filename='cms/js/cms_base.js') }}"></script>
{% block head %}
{% endblock %}
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">论坛CMS管理系统</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<!-- 从数据库中调用用户名,g对象全局调用g.cms_user对象 .username是该对象的一个字段属性 -->
<li><a href="#">{{ g.cms_user.username }}<span>[超级管理员]</span></a></li>
<!-- 用户注销,关联到views.py中的@cms_bp.route("/logout/")路由,重定向到该路由 -->
<li><a href="{{ url_for('cms.logout') }}">注销</a></li>
</ul>
<form class="navbar-form navbar-right">
<input type="text" class="form-control" placeholder="查找...">
</form>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav-sidebar">
<li class="unfold"><a href="#">首页</a></li>
<li class="profile-li">
<a href="#">个人中心<span></span></a>
<ul class="subnav">
<!-- url重定向到/cms/profile/下 这个路由在views.py中定义了 -->
<li><a href="{{ url_for('cms.profile') }}">个人信息</a></li>
<li><a href="#">修改密码</a></li>
<li><a href="#">修改邮箱</a></li>
</ul>
</li>
<li class="nav-group post-manage"><a href="#">帖子管理</a></li>
<li class="comments-manage"><a href="#">评论管理</a></li>
<li class="board-manage"><a href="#">板块管理</a></li>
<li class="nav-group user-manage"><a href="#">用户管理</a></li>
<li class="role-manage"><a href="#">组管理</a></li>
<li class="nav-group cmsuser-manage"><a href="#">CMS用户管理</a></li>
<li class="cmsrole-manage"><a href="#">CMS组管理</a></li>
</ul>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1>{% block page_title %}
{% endblock %}</h1>
<div class="main_content">
{% block content %}
{% endblock %}
</div>
</div>
</div>
</div>
</body>
</html>
基础模板文件创建后,其他的cms_xxx.html文件就可以进行模板继承,不要大量的代码来渲染页面了
修改模板文件templates/cms/cms_index.html文件
<!-- 继承模板文件cms/cms_base.html 简化代码 -->
{% extends 'cms/cms_base.html' %}
<!-- 页面标题 -->
{% block title %}
CMS管理系统
{% endblock %}
<!-- 标题 -->
{% block page_title %}
欢迎来到CMS管理系统
{% endblock %}
<!-- 内容 -->
{% block content %}
<!-- 引用相同的内容 -->
{{ self.page_title() }}
{% endblock %}
模板文件templates/cms/cms_profile.html文件
<!-- 继承模板文件cms/cms_base.html 简化代码 -->
{% extends 'cms/cms_base.html' %}
<!-- 页面标题 -->
{% block title %}
个人信息
{% endblock %}
<!-- 标题 -->
{% block page_title %}
{{self.title()}}
{% endblock %}
{% block content %}
<!-- 将全局变量的对象在这个block模块中命名为user -->
{% set user = g.cms_user %}
<table class="table table-bordered">
<tr>
<td>用户名:</td>
<!-- 调用全局变量对象的username字段属性 user替代g.cms_user -->
<td>{{ user.username }}</td>
</tr>
<tr>
<td>邮箱:</td>
<td>{{ user.email }}</td>
</tr>
<tr>
<td>角色:</td>
<td>暂未实现</td>
</tr>
<tr>
<td>权限:</td>
<td>暂未实现</td>
</tr>
<tr>
<td>加入时间:</td>
<td>{{ user.join_time }}</td>
</tr>
</table>
{% endblock %}
因为需要在templates/cms/cms_profile.html文件中调用到登录信息中的session的信息,所以需要针对apps/cms/views.py文件和apps/cms/hooks.py进行修改
视图文件apps/cms/views.py
# -*- encoding: utf-8 -*-
"""
@File : views.py
@Time : 2020/5/11 9:59
@Author : chen
"""
# 蓝图文件:实现模块化应用,应用可以分解成一系列的蓝图 后端的类视图函数写在这个文件
from flask import Blueprint, render_template, views, session # 定义类视图,显示模板文件
from flask import request, redirect, url_for # 页面跳转redirect request请求收集
# 导入form表单 .forms代表同级目录下的forms.py
from .forms import LoginForm
# 导入模型 .models代表同级目录下的models.py
from .models import CMS_User
# 导入装饰器:判断当前界面是否是登录界面,不是就将url重定向到登录界面
from .decorators import login_required
cms_bp = Blueprint("cms", __name__, url_prefix='/cms/') # URL前缀url_prefix
# 钩子函数是在cms_bp创建之后才创建的,顺序在cms_bp创建之后
from .hooks import before_request
@cms_bp.route("/") # 后台界面
# @login_required # 装饰器判定当前界面是否是登录界面,但是需要每个路由函数都要加该装饰器,比较麻烦,推荐使用钩子函数
def index():
# return "cms index:后端类视图文件"
return render_template('cms/cms_index.html') # 登陆之后进入CMS后台管理界面,路径写全cms/cms_index.html
# 用户注销登录
@cms_bp.route("/logout/") # 需要关联到cms/cms_index.html中的注销属性
def logout():
# session清除user_id
del session['user_id']
# 重定向到登录界面
return redirect(url_for('cms.login')) # 重定向(redirec)为把url变为重定向的url
# 定义个人中心的路由
@cms_bp.route("/profile/")
def profile():
return render_template("cms/cms_profile.html") # 模板渲染(render_template)则不会改变url,模板渲染是用模板来渲染请求的url
# 定义类视图,显示模板文件 用户登录功能实现
class LoginView(views.MethodView):
def get(self, message=None): # message=None时候不传输信息到cms_login.html页面
return render_template("cms/cms_login.html", message=message) # 针对post方法中同样要返回到cms_login.html页面进行代码简化
# 用户登录操作验证
def post(self):
# 收集表单信息
login_form = LoginForm(request.form)
if login_form.validate():
# 数据库验证
email = login_form.email.data
password = login_form.password.data
remember = login_form.remember.data
# 查询数据库中的用户信息
user = CMS_User.query.filter_by(email=email).first() # 邮箱唯一,用于查询验证用户
if user and user.check_password(password): # 验证用户和密码是否都正确
session['user_id'] = user.id # 查询到用户数据时,保存session的id到浏览器
# session['user_name'] = user.username # 将数据库中的user.username保存到session中,在hooks.py中判断
# session['user_email'] = user.email # 将数据库中的email保存到session中,方便html调用信息
# session['user_join_time'] = user.join_time # 将数据库中的join_time保存到session中,方便html调用信息
if remember: # 如果用户点击了remember选择,在浏览器中进行数据持久化
session.permanent = True # 数据持久化,默认31天,需要设置session_key在config.py中
# 登录成功,跳转到后台首页
return redirect(url_for('cms.index')) # 在蓝图中必须加cms 跳转到index方法
else:
# return "邮箱或密码错误" # 登录出错,返回结果
# return render_template("cms/cms_login.html", message="邮箱或密码错误") # 登录出错,返回结果渲染到cms_login.html页面
return self.get(message="邮箱或密码错误") # 传参到get方法中,多加一个传输错误信息的参数到方法中
else:
print(login_form.errors) # forms.py中的错误信息 字典类型数据
print(login_form.errors.popitem()) # forms.py中的错误信息 元祖类型数据
# return "表单验证错误" # 错误信息需要渲染到cms_login.html页面
return self.get(message=login_form.errors.popitem()[1][0]) # 字典类型数据信息提取
# 添加登录路由
cms_bp.add_url_rule("/login/", view_func=LoginView.as_view('login')) # view_func 命名操作名字,"/login/"路由地址
视图文件apps/cms/hooks.py
# -*- encoding: utf-8 -*-
"""
@File : hooks.py
@Time : 2020/5/13 9:36
@Author : chen
"""
from flask import request, session, url_for, redirect, g # g对象全局变量gloabl,方便调用
from .views import cms_bp
from .models import CMS_User
# 钩子函数 ,所有操作前执行该方法,判断当前界面是否是登录界面,不是就将url重定向到登录界面
@cms_bp.before_request
def before_request():
print(request.path) # 输出的是网页url的后缀,即/cms/login/
if not request.path.endswith(url_for('cms.login')): # 判断当前所在url是否是/cms/login/,不是代表不在后台登录界面
user_id = session.get('user_id') # 登陆之后,获取登录时候记录的session中的user_id
if not user_id: # 若没有user_id,说明登录不成功
return redirect(url_for('cms.login')) # 重定向到后台登录界面
# 判断user_id是否登陆过,登录之后就返回用户名到CMS后台管理系统
if 'user_id' in session:
user_id = session.get('user_id') # 调用session中user_id
user = CMS_User.query.get(user_id) # 通过user_id查询到用户对象,方便前端界面调用对象中的字段属性
if user:
g.cms_user = user # 赋值给g对象,全局变量g.cms_user用于渲染到后台管理界面cms_index.html
# 上面的代码相对于下面的来说比较简单,下面的是将对象中的字段属性单独来调用并修改为全局变量,上面只是将完整的一个对象变成全局变量
'''
# 判断user_id是否登陆过,登录之后就返回用户名到CMS后台管理系统
if 'user_id' in session: # user_id在session中,说明cms用户已经登录了
user_name = session.get('user_name') # 从session中调用user_username
user_email = session.get('user_email') # 从session中调用user_email,用于设置为全局变量,渲染到cms_profile.html中
user_join_time = session.get('user_join_time') # 从session中调用user_join_time,用于设置为全局变量,渲染到cms_profile.html中
if user_name:
g.cms_user = user_name # 赋值给g对象,全局变量g.cms_user用于渲染到后台管理界面cms_index.html
g.cms_email = user_email # 设置为全局变量,渲染到cms_profile.html中
g.cms_join_time = user_join_time # 设置为全局变量,渲染到cms_profile.html中
'''
实现效果如下: