原文: http://www.catonlinepy.tech/
声明 :原创不易,未经许可,不得转载
接第 5 天上篇的内容,我们接着介绍用户注册功能的实现
3. 用户的注册
3.1 注册表单及前端渲染
根据经验,当我们在登录某个应用时,如果我们没有在平台注册过账号,平台会提醒我们注册一个账号,注册成功后方可进行登陆。与登录页面的实现类似,前端的注册页面也需要后台传入注册表单,然后 Jinjia2 将表单渲染成前端浏览器能解析的 form 表单。由于注册页面通常要求用户输入电子邮箱,用户名,密码以及确认密码,下面我们首先在 forms.py 文件中增加注册表单类 registerForm:
# forms.py 文件中新增的内容
# ..
# 在 wtforms 中还需要导入 StringField,ValidationError 字段
from wtforms import PasswordField, BooleanField, SubmitField, StringField,ValidationError
# 在 validators 验证器中还需要导入 EqualTo, InputRequired 验证器
from wtforms.validators import DataRequired, Length, EqualTo, InputRequired
# 在 def 两个函数中使用到 User 模型,需要导入
from userauth_demo.models import User
# ..
class registerForm(FlaskForm):
username = StringField(u'用户名',
validators=[DataRequired(), Length(min=2, max=20)])
email = EmailField(u"邮箱",
validators=[DataRequired(), Length(min=2, max=20)])
password = PasswordField(u'密码',
validators=[InputRequired(), EqualTo(u"confirm_password", message="两次输入的密码不一致!")])
confirm_password = PasswordField(u'确认密码',
validators=[DataRequired()])
submit = SubmitField(u'注册')
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user is not None:
raise ValidationError('该用户名已被注册!')
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('该邮箱已被注册!')
在上面的注册表单类 registerForm 中,为了减少输入错误的风险,使用到了 password 和 confirm_password,password 密码中用到了 EqualTo 验证器,它需要与 confirm_password 中输入的密码一致才通过验证。这个注册表单中还使用到了两个自定义函数,它们以 validate 开头,后面接上_<fieldname>,这里的 filedname 都是 WTForms 提供的自定义字段,当前端用户点提交按键时,就会触发这两个函数的调用。在这里,当用户提供了注册信息后,会在数据库中的 user 表中进行查询,如果用户名和邮箱不存在,则可顺利的完成注册,否则,就会执行 if 语句内的错误提醒程序,这些错误提醒会在浏览器页面渲染出来。
注册表单写好后,需要在前端 HTML 模板中渲染表单,它与登陆功能的模板类似,在 register.html 文件中输入如下内容:
<!-- register.html 文件中的内容 -->
{% extends "layout.html" %}
{% block content %}
<h2> 注册 </h2>
<form action=""method="post">
<!-- 防跨站伪造请求 -->
{{html_form.hidden_tag() }}
<div>
{{html_form.username.label}}<br />
{{html_form.username() }}
{% if html_form.username.errors %}
<div >
{% for error in html_form.username.errors %}
<span>{{error}}</span>
{% endfor %}
</div>
{% endif %}
</div>
<div>
{{html_form.email.label}}<br />
{{html_form.email() }}
{% if html_form.email.errors %}
<div >
{% for error in html_form.email.errors %}
<span>{{error}}</span>
{% endfor %}
</div>
{% endif %}
</div>
<div>
{{html_form.password.label}}<br />
{{html_form.password() }}
</div>
<div>
{{html_form.confirm_password.label}}<br />
{{html_form.confirm_password() }}
</div>
<div>
{{html_form.submit() }}
</div>
</form>
<!-- 添加跳转到登陆页面的超链接 -->
<p>
已经有账户? <a href="{{url_for('login')}}"> 登陆 </a>
</p>
{% endblock content %}
3.2 为注册表单编写路由函数
在编写路由函数之前,先来讲讲密码哈希的重要性。因为在注册页面中,用户需要输入密码,而密码最终需要存入到数据库中的,但是如果把明文密码存到数据库,这是一件很危险的事儿。假如数据库被黑客获取后(脱裤),用户的个人账号、密码就完全泄漏给了黑客。这时,就需要有工具将密码加密后再写到数据库。flask_bcrypt 就是干这个活儿的插件,负责对明文密码进行加密操作,下面先在虚拟环境中安装这个插件:
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day5$ pip install Flask_Bcrypt
安装完成后,我们先在 python shell 中练练手,熟悉熟悉这个插件的基本用法:
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day5$ python
Python 3.6.7 (default, Oct 22 2018, 11:32:17)
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from flask_bcrypt import Bcrypt # 从 flask_bcrypt 中导入 Bcrypt 类
>>> bcrypt = Bcrypt() # 对 Bcrypt() 进行实例
>>> bcrypt.generate_password_hash('miaojie') # 产生哈希密码
b'$2b$12$4D2eChUFV45DkC/VMsc0.u3JSjkSPvSZHHtokDHmrhD7tnKldFf/a'
>>> bcrypt.generate_password_hash('miaojie').decode('utf-8') # 对哈希密码进行解码
'$2b$12$Q8.Io9ld5mwXiCKc2TiWI.8c7FPDwh440zo80NkwUwvtebXy.8xoO'
>>> bcrypt.generate_password_hash('miaojie').decode('utf-8') # 每次产生的密码会不一样,这使得得通过哈希过的密码无法查看它原始的密码
'$2b$12$YXKa1TSyROPvpprL7UfjOOoWdq16z/ZH2n8dpn8tCc8nDTwNoB.Jm'
>>> hashed_password = bcrypt.generate_password_hash('miaojie').decode('utf-8')
>>> bcrypt.check_password_hash(hashed_password,'password') # 哈希过的密码和明文密码进行对比,匹配则返回 True, 不匹配则返回 False
False
>>> bcrypt.check_password_hash(hashed_password,'miaojie')
True
>>>
熟悉了 flask_bcrypt 插件后,我们就可以使用它了,在__init__.py 文件中添加 bcrypt 对象,如下所示:
# 在__init__.py 文件中新增的内容
# ..
# 从 flask_bcrypt 中导入 Bcrypt 类
from flask_bcrypt import Bcrypt
# Bcrypt() 的实例
bcrypt = Bcrypt()
# ..
在 routes.py 文件中新增注册表单的路由函数 register,如下所示:
# ..
from userauth_demo.forms import registerForm
from userauth_demo import db,bcrypt
from userauth_demo.models import User
# ..
@app.route("/register", methods=["GET", "POST"])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = registerForm()
# 发送 post 请求
if form.validate_on_submit():
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
user = User(username=form.username.data, email=form.email.data, password=hashed_password)
db.session.add(user)
db.session.commit()
flash('该用户名已注册,请登陆!', 'success')
return redirect(url_for('login'))
return render_template('register.html', title="第五天", html_form=form)
当用户填写完注册信息,点击注册按键后,就会向后台发送 post 请求,后台接收到 post 请求后,会将用户名、邮箱以及哈希后的密码保存到数据库中。注册成功后,提示 flash 消息,并重定向到登陆页面。如果是发送 get 请求,则直接会对模板进行渲染,显示出注册页面的表单。
通过 python run.py 将 web 程序拉起,开始验证结果,在浏览器中输入 http://127.0.0.1:5005/register,效果图如下所示:
如图,在注册表单中输入用户名、邮箱及密码,点击注册按钮后,如果注册成功,就会跳转到下图所示的登陆页面。
4. 真正的登录过程
上面我们在注册页面的最下方,添加了跳转到登陆页面的超链接,这里也需要在登陆页面的最下方添加跳转到注册页面的超链接,方便未注册的用户快速找到注册页面。这里在 login.html 文件的 <form></form> 标签下方添加注册超链接:
<! --login.html 文件中添加注册链接 -->
# ..
<p>
没有账号? <a href="{{url_for('register')}}"> 注册 </a>
</p>
# ..
效果如下所示:
4.1 登陆用户
在第 5 天上篇的课程中,我们完成了登陆表单路由函数的实现。其中,当用户发送 post 请求时,后台直接使用了假的邮箱和密码(不是从数据库中读取出来的用户信息)进行验证。但是真实的登录过程中,后台程序通过用户输入的邮箱,从数据库中读取出用户的密码信息,然后将数据库中的密码与用户在前端输入的密码进行比较,如果用户输入的密码是正确的,就将该用户标记为登录状态,否则就提示用户密码输入错误。修改 routes.py 文件的内容,添加用户登录认证的代码:
# routes.py 文件中修改登陆表单路由函数
# ..
# 从 userauth_demo 中导入 bcrypt 实例
from userauth_demo import db, bcrypt
# 从 flask_login 中导入 login_user 函数
from flask_login import current_user, login_user
# 从 userauth_demo.models 中导入 User 模型
from userauth_demo.models import User
#..
@app.route('/login', methods=['GET','POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
# 对类 LoginForm 的实例
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and bcrypt.check_password_hash(user.password, form.password.data):
login_user(user, remember=form.remember.data)
else:
flash('登陆不成功,请检查邮箱和密码!', 'danger')
return render_template('login.html', title='第五天', html_form=form)
# ..
我们对上面代码的工作流程进行简单的讲解:首先,用户发送 post 请求后,后台接收到该请求后,通过用户输入的邮箱从数据库中找到该用户,再使用 first() 方法返回第一个匹配的用户。接下来的 if 语句判断该用户是否存在,并且使用 check_password_hash 函数验证用户输入的密码是否和数据库中的密码一致,如果条件都满足则会执行 login_user() 函数,并将用户登陆状态记为已登陆,否则,会执行 else 语句,flash 消息提示登陆不成功,请检查邮箱和密码。
4.2 页面访问权限控制
web 开发过程中,经常会碰到网页权限控制的问题,即有些页面需要登录状态才能访问,Flask_Login 直接为我们提供了该功能:如果用户访问的页面需要登录权限,而此时用户不是处于登录状态,则会强制用户跳转到登录页面。为了实现这个功能,Flask_Login 需要知道哪个视图函数用于处理登陆机制,在__init__.py 文件中的添加如下代码:
# 在__init__.py 文件中添加内容
login_manager = LoginManager(app)
login_manager.login_view = 'login' #login 是登录页面视图函数的函数名
Flask_Login 使用 @login_required 装饰器让已通过认证的用户进行访问,如果将该装饰器放到 @app.route 装饰器下的视图函数上面时,则该路由函数受到保护,不会让匿名(未登录)的用户进行访问,Flask-Login 会拦截请求,把用户发往登录页面。对 routes.py 文件进行修改,如下所示:
# routes.py 文件中添加内容
# ..
# 从 flask_login 中导入 login_required 类
from flask_login import current_user, login_user, login_required
# ..
@app.route('/')
# 在主页的视图函数上面加上 @login_required 装饰器
@login_required
def index():
return render_template('index.html', title='第五天')
此时,我们在未登录状态下访问主页 /,则会跳转到如下图所示的登录页面:
login_required 装饰器拦截请求并将主页重定向到登录页面,此时 URL 中还包含其它的信息,如 /login?next=%2F。next 参数可以帮助用户在登录成功后,直接跳转到之前没有权限访问的页面。下面这段代码,展示了如何查询和处理 next 参数,在 routes.py 中进行修改:
# routes.py 文件中添加内容
# 从 flask 中导入 request
from flask import render_template, redirect, url_for, flash, request
# ..
@app.route('/login', methods=['GET','POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and bcrypt.check_password_hash(user.password, form.password.data):
login_user(user, remember=form.remember.data)
# 查询 next 参数
nextpage = request.args.get('next')
# 对 next 参数进行处理
if nextpage:
return redirect(nextpage)
else:
url = url_for('index')
return redirect(url)
else:
flash('登陆不成功,请检查邮箱和密码!', 'danger')
return render_template('login.html', title='第五天', html_form=form)
# ..
request.args.get 可以得到 next 的参数赋值给 nextpage,下面的 if 语句判断 nextpage 是否存在,如果存在,则直接重定向到 nextpage 所在页面,如果不存在,则会重定向到主页。
下面,我们在登录页面输入正确的用户邮箱和密码,点击登陆按钮后页面将重定向到主页,效果如下图所示:
4.3 用户注销
通常当用户登录后,想要退出登录状态,那么注销功能也是必须的,借用 Flask_Login 的 logout_user() 函数,可以轻松的实现用户的注销功能。在 routes.py 文件中添注销功能的代码,如下:
# routes.py 文件中添加的内容
# ..
# 从 flask_login 中导入 logout_user 类
from flask_login import current_user, login_user, login_required, logout_user
# ..
# 添加注销功能的视图函数
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
此外,还需要在基模板中添加注销的前端代码,修改 layout.html 文件中的内容:
<! -- layout.html 文件中添加内容 -->
# ..
<header>
<div>
<a href="{{url_for('index') }}"> 主页 </a>
{% if current_user.is_authenticated() %}
<a href="{{url_for('logout') }}"> 注销 </a>
{% else %}
<a href="{{url_for('login') }}"> 登陆 </a>
{% endif %}
</div>
</header>
#..
添加完以上代码后,我们只需刷新一下主页,就会在主页看到新增的注销按钮,效果图如下所示:
5. 总结
学习完今天的内容,我们掌握了如下技能:
- 学习了 Flask_Login 插件的基本使用方法
- 学习了如何对明文密码进行加密、比对
- 学习了如何利用数据库完成用户注册功能
- 学习了如何实现用户的登陆及页面权限管理
- 学习了如何控制用户的登录状态和注销状态
下一课的教程,猫姐将带领大家一起学习个人主页的实现及用户头像功能的管理。今天的内容就到这里,喜欢的同学们可以在下面点赞留言,或是访问我的博客地址:http://www.catonlinepy.tech/ 加入我们的 QQ 群进一步交流学习!
6. 代码的获取
大家可以到 github 上获取今天教程的所有代码:https://github.com/miaojie19/…
具体下载代码的命令如下:
# 使用 git 命令下载 flask-course-primary 仓库所有的代码
git clone https://github.com/miaojie19/flask-course-primary.git
# 下载完成后,进入 day5 目录下面,即可看到今天的代码
cd flask-course-primary
cd day5