Flask-BBS论坛项目实战2-cms用户模型定义及后台登录功能

编辑cms.models.py

1
2
3
4
5
6
7
8
9
10
from exts import db
from datetime import datetime

class CMSUser(db.Model):
__tablename__ = 'cms_user'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(50), nullable=False)
password = db.Column(db.String(100), nullable=False)
email = db.Column(db.String(50), nullable=False, unique=True) #unique表示必须是唯一的
join_time = db.Column(db.DateTime, default=datetime.now )

把cms.models导入到manage.py中, 编辑manage.py

1
from apps.cms import models as cms_models

生成迁移脚本并映射到数据库中:

1
2
python manage.py db migrate
python manage.py db upgrade

到此我们就把迁移脚本映射到数据库中了,我们需要查看数据库中有没有这个数据,每次都在虚拟机中切换很麻烦,但是pycharm给我们一个很好用的功能,点击pycharm右侧的database按钮

点击“+”号,选择mysql

输入地址,用户,密码,和数据库名称,点击链接即可

然后就可以在pycharm中查看我们创建的数据库了

给cms添加个管理用户

编辑manage.py,配置创建用户的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...

CMSUser = cms_models.CMSUser

@manager.option('-u','--username',dest='username')
@manager.option('-p','--password',dest='password')
@manager.option('-e','--email',dest='email')
def create_cms_user(username, password, email):
user = CMSUser(username=username, password=password, email=email)
db.session.add(user)
db.session.commit()
print('用户创建成功')


...

创建用户:

1
2
python manage.py create_cms_user -u admin -p 123456 -e admin@bbs.com
创建用户成功


用户创建成功了,但是我们的密码却是明文的,这是非常危险的!
明文密码是万万不可以的,因此我们需要改造下models

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from exts import db
from datetime import datetime
from werkzeug.security import generate_password_hash,check_password_hash # 导入加密方法和解密方法
class CMSUser(db.Model):
__tablename__ = 'cms_user'
id = db.Column(db.Integer,primary_key=True,autoincrement=True)
username = db.Column(db.String(50),nullable=False)
_password = db.Column(db.String(100),nullable=False) # 把password改为_password,用户就不能直接给此字段赋值了,因为用户面对的还是password
email = db.Column(db.String(50),nullable=False,unique=True)
join_time = db.Column(db.DateTime,default=datetime.now)

def __init__(self,username,password,email):
# 因为此模型已经没有password了,用户面对的还是password,所有这里要定义个__init__
self.username = username
self.password = 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)

def check_password(self,raw_password):
# 验证密码
result = check_password_hash(self.password,raw_password)
return result

更新模型到数据库

1
2
python manage.py db migrate
python manage.py db upgrade

再创建一个用户

1
python manage.py create_cms_user -u 111111 -p 111111 -e admin@123.com

这是我们的密码就被加密了

cms后台登录界面

后台登录页面,我们不用自己写,只需要去Bootstrap中文网去找一个模板改一下就行
这里使用的模板是:https://v3.bootcss.com/examples/signin/
点击右键查看网页源码,把源码复制下载
在项目根目录下新建templates目录
然后在templates下新建目录cms
在cms目录下新建文件cms_login.html,并把源码复制到该文件中
cms_login.html会用到样式文件signin.css
在项目static目录下新建目录 cms/css
在static/cms/css新建文件signin.css, 并把源码拷贝进去

static/cms/css/sigin.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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;
}
修改cms_login.html, 修改后如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!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="">

<title>登录-论坛CMS管理系统</title>

<!-- Bootstrap core CSS -->
<link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">


<!-- Custom styles for this template -->
<link href="{{ url_for('static', filename='cms/css/signin.css') }} " rel="stylesheet">

</head>

<body>

<div class="container">

<form class="form-signin" method="post">
<h2 class="form-signin-heading">请登录</h2>
<label for="inputEmail" class="sr-only">邮箱</label>
<input type="email" id="inputEmail" class="form-control" placeholder="邮箱" required autofocus name="email">
<label for="inputPassword" class="sr-only">密码</label>
<input type="password" id="inputPassword" class="form-control" placeholder="密码" required name="password">
<div class="checkbox">
<label>
<input type="checkbox" value="1" name="remember"> 记住我
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">登录</button>
</form>

</div> <!-- /container -->

</body>
</html>
编写cms登录视图,编辑cms.views.py
1
2
3
4
5
6
7
8
9
10
11
...

from flask import Blueprint, views, render_template

class LoginView(views.MethodView):
def get(self):
return render_template('cms/cms_login.html')
def post(self):
pass

bp.add_url_rule('/login/', view_func=LoginView.as_view('login'))

启动访问http://127.0.0.1:5000/cms/login/

cms后台登录功能

后台登录,前端就需要提交数据给我们,那么首先,我们就需要form验证
安装flask-wtf

1
pip install flask-wtf

编辑cms.forms.py
1
2
3
4
5
6
7
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(6,20, message='密码长度为6-20位')])
remember = IntegerField() #这个值不用验证,这里只是接收

然后就可以写视图了,编辑cms.views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from flask import Blueprint, views, render_template, request, session
from flask import redirect, url_for
from .forms import LoginForm
from .models import CMSUser

bp = Blueprint('cms', __name__, url_prefix='/cms')

@bp.route('/')
def index():
return 'cms index'

class LoginView(views.MethodView):
def get(self, message=None):
return render_template('cms/cms_login.html', message=message)

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 = CMSUser.query.filter_by(email=email).first()
if user and user.check_password(password):
session['user_id'] = user.id
if remember:
#如果勾选了记住我,则保存session,这样就算浏览器关闭session还是存在的
session.permanent = True
return redirect(url_for('cms.index')) #因为是蓝图这里必须使用cms.index,不能使用index
else:
#return render_template('cms/cms_login.html', message='账号或密码错误')
#等同于以下代码
return self.get(message='账号或密码错误')

else:
#login_form.errors是一个字典,如{"email":['邮箱格式错误'], "password":["密码长度为6-20位"]}
#login_form.errors.popitem() 是取出字典的任意一项,结果是元组,如:("password":["密码长度为6-20位"])
#取出该元组的第2个元素:login_form.errors.popitem()[1], 如:["密码长度为6-20位"]
#最后取出错误提示语:login_form.errors.popitem()[1][0]
message = login_form.errors.popitem()[1][0]
return self.get(message=message)


bp.add_url_rule('/login/', view_func=LoginView.as_view('login'))

因为用到了session,所以还需要在配置文件,配置一个key为session加密用

编辑config.py
1
2
3
4
5
6
7
import os
from datatime import timedelta

#session
SECRET_KEY = os.urandom(24)
#设置session有效期为2天,若开启了session.permanent后不设置该参数,则默认为31天
PERMANENT_SESSION_LIFETIME = timedelta(days=7)

前端代码需要加上错误提示,首先要判断message是否存在

cms_login.html
1
2
3
4
5
6
...
<button class="btn btn-lg btn-primary btn-block" type="submit">登录</button>
</form>
{% if message %}
<p style="text-align: center" class="text-danger">{{ message }}</p>
{% endif %}


这样就有提示了。
访问输入正确的邮箱,就可以登录成功,跳转的http://127.0.0.1/cms/ 并且保存用户session到cookie了

CMS后台登录限制

限制存在个问题,就是我们没有登录的情况下,也能够访问cm后台的首页http://127.0.0.1/cms/ 接下来我们就要给它加上限制,只有登录后才能访问。
要实现这个功能,可以使用装饰器。
首先在cms下面新建个文件decorators.py, 以后此蓝图下的所有装饰器都放到这个文件里面

1
2
3
4
5
6
7
8
9
10
11
12
from flask import session, redirect, url_for
from functools import wraps

def login_required(func):
@wraps(func) # 保留func的属性
def inner(*args, **kwargs):
if 'user_id' in session:
return func(*args, **kwargs)
else:
#session中没有user_id表示没有登录,则跳转到登录页面
return redirect(url_for('cms.login'))
return inner

然后在cms首页视图加上login_required装饰器

1
2
3
4
5
6
7
8
...

from .decorators import login_required

@bp.route('/') #路由装饰器一定要放在最上面,否则会找不到路由
@login_required
def index():
return 'cms index'

这样,当我们在没有登录的情况下访问http://127.0.0.1/cms/ 会自动跳转到登录页面了.

代码优化

优化一

可以发现,我们把用户的session信息(user.id)保存在session[‘user_id’]中,在登录装饰器中也用到.
这样做有几个缺点:
1. 当我们fornt也用到session的时候也这样用user_id 就不好区分
2. 因为多处都用这个这个值,很容易写错
所以,我们可以把它写成一个常量来用,编辑config.py

config.py
1
CMS_USER_ID = LULULULU'

然后在用到user_id的地方都import config ,然后使用CMS_USER_ID

优化二

当表单验证不通过以后,我们是如下实现的,随机抽取一个错误信息出来(实际项目中可能要列出所有的错误信息,要根据需求来定)

1
message = login_form.errors.popitem()[1][0]

我们可以把获取错误信息的代码写到 form中,这样就避免每次都写这么长的代码,也避免容易犯错

1
2
3
4
5
6
7
8
9
class LoginFrom(Form):
email = StringField(validators=[Email(message='邮箱格式错误'),InputRequired(message='请输入邮箱') ])
password = StringField(validators=[Length(6,30, message='密码长度6-30')])
remember = IntegerField()

#添加获取错误信息的方法
def get_error(self):
message = self.errors.popitem()[1][0]
return message

然后我们视图中调用这个方法就可以了

1
message = login_form.get_error()

因为项目中以后还会有其他的form验证,那么每个form中都要加这个方法,这样也比较麻烦,所以,我们可以进一步优化。我们可以使用继承来解决

在apps/下面新建一个forms.py

apps/forms.py
1
2
3
4
5
6
from wtforms import Form

class BaseForm(Form):
def get_error(self):
message = self.errors.popitem()[1][0]
return message

以后我们写的form继承BaseForm就拥有get_error方法了

1
2
3
4
class LoginFrom(BaseForm):
email = StringField(validators=[Email(message='邮箱格式错误'),InputRequired(message='请输入邮箱') ])
password = StringField(validators=[Length(6,30, message='密码长度6-30')])
remember = IntegerField()

CSRF保护

编辑主程序文件bbs.py,开启csrf

bbs.py
1
2
3
4
 ...
from flask_wtf import CSRFProtect

CSRFProtect(app)

加上csrf保护后,我们再尝试登陆就会失败

因为还需在提交表单的 form中添加一个input携带csrf_token给服务器验证

cms_login.html
1
2
3
4
5
<form class="form-signin" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
...

</form>

后台界面搭建

我们需要用到前端模板。^_^..如果需要模板素材的同学发送到邮箱wouldmissyou@163.com,写明索取flask论坛素材即可,博主收到邮件后会第一时间发送,有问题也可以互相探讨哦。
这里我们需要用到cms后台模板.

  • 在static/cms下再新建个目录js用来存放cms下的所有js文件
  • 把后台模板css里面的文件拷贝到static/cms/css/
  • 把后台模板js里面的文件拷贝到static/cms/js/
  • 把cms_index.html拷贝到templates/cms/

编辑cms_index.html,修改静态文件路径(css、js)
这里只演示一个例子,例如:

1
2
3
<script src="js/cms_base.js"></script>
改为
<script src="{{ url_for('static', filename='cms/js/cms_base.js') }}">

编辑视图,cms.views.py

cms.views.py

1
2
3
4
5
6
...

@bp.route('/')
@login_required
def index():
return render_template('cms/cms_index.html')

-------------本文结束感谢您的阅读-------------