任务清单管理系统(二)
项目地址:https://github.com/lvah/TodoList

我们的项目的一个主要流程:

用户第一次访问我们的网站,首先要注册,每个用户只能注册一个账户,我们通过邮箱来验证。
注册的时候通过邮箱验证 confirm给邮箱地址发送一个地址,验证成功,就可以登陆
登陆过程中我们先判断邮箱有没有验证,
如果有验证,则允许访问任务清单管理系统,添加或者删除任务等;
如果没有验证,我们会返回一个邮箱没有验证的页面,点击链接进行重新验证,我们会给邮箱重新发送一个邮件进行验证。
一、用户认证
大多数程序都要进行用户跟踪。用户连接程序时会进行身份认证,通过这一过程,让程序知道自己的身
份。程序知道用户是谁后,就能提供有针对性的体验。
1.数据库模型
(1)技术要点
Werkzeug 中的 security 模块能够很方便地实现密码散列值的计算。这一功能的实现只需要
两个函数,分别用在注册用户和验证用户阶段。
generate_password_hash(password, method= pbkdf2:sha1 , salt_length=8):密码加密的散列值。- generate_password_hash(明文密码,加密方法,加密盐长度)
check_password_hash(hash, password):密码散列值和用户输入的密码是否匹配.
测试hash加密:
加密方法:在密码后面加一系列长度,在对密码进行哈希加密

@property 是经典类中的一种装饰器,新式类中具有三种:

(2)核心代码
# app/models.py
'''
设计数据库模型:
1). 用户信息: User
2). 用户角色信息: Role
3). 用户角色: 用户 = 1:n, 一对多关系,外键写在多的一端。
'''
from werkzeug.security import generate_password_hash, check_password_hash
from app import db
from flask_login import UserMixin
from . import login_manager
'''
Flask-Login 提供了一个 UserMixin 类,包含常用方法的默认实现,且能满足大多数需求。
1). is_authenticated 用户是否已经登录?
2). is_active 是否允许用户登录?False代表用户禁用
3). is_anonymous 是否匿名用户?
4). get_id() 返回用户的唯一标识符
'''
# Flask中一个Model子类就是数据库中的一个表。默认表名'User'.lower() ===> user
class User(UserMixin,db.Model):
__tablename__ = 'users' # 自定义数据表的表名
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(100), unique=True, nullable=False)
password_hash = db.Column(db.String(200), nullable=True)
# 电子邮件地址email,相对于用户名而言,用户更不容易忘记自己的电子邮件地址。
email = db.Column(db.String(64), unique=True, index=True)
# 外键关联
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
@property
def password(self):
raise AttributeError('password is not a readable attribute')
@password.setter
def password(self, password):
# generate_password_hash(password, method= pbkdf2:sha1 , salt_length=8):密码加密的散列值。
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
# check_password_hash(hash, password) :密码散列值和用户输入的密码是
return check_password_hash(self.password_hash, password)
def __repr__(self):
return "<User: %s>" % (self.username)
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(100), unique=True, nullable=False)
# 做了两件事情: 1). Role添加属性users 2). User添加属性role
users = db.relationship('User', backref='role')
def __repr__(self):
return "<Role: %s>" % (self.name)
- manage中 数据库迁移 和shell 添加
# manage
from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager, Shell
from app import create_app, db
# 动态创建app对象
app = create_app('default')
# 脚本管理
manager = Manager(app)
# 将数据库迁移插件与数据库db和app关联
migrate = Migrate(app, db)
def make_shell_context():
return dict(app=app, db=db)
if __name__ == '__main__':
# 初始化 Flask-Script、Flask-Migrate 和为 Python shell 定义的上下文。
manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)
manager.run()
添加成功:


提交数据库
python manage.py db init 执行了之后是在目录中新增了一个文件,里面存储对代码所有更改的版本
python manage.py db migrate:当数据库代码有所更改时,执行这个代码,在目录中生成一个新的文件,打开文件,里面记录所更改的内容。def upgrade()是其中一个函数,
执行python manage.py db upgrade代码,就是提交到数据库
# python manage.py db init 执行了之后是在目录中新增了一个文件,里面存储对代码所有更改的版本
(base) F:\ziliao\python_kaifa\my_code\09_\TodoList>python manage.py db init
Creating directory F:\ziliao\python_kaifa\my_code\09_\TodoList\migrations ... done
Creating directory F:\ziliao\python_kaifa\my_code\09_\TodoList\migrations\versions ...
done
Generating F:\ziliao\python_kaifa\my_code\09_\TodoList\migrations\alembic.ini ... done
Generating F:\ziliao\python_kaifa\my_code\09_\TodoList\migrations\env.py ... done
Generating F:\ziliao\python_kaifa\my_code\09_\TodoList\migrations\README ... done
Generating F:\ziliao\python_kaifa\my_code\09_\TodoList\migrations\script.py.mako ... do
ne
Please edit configuration/connection/logging settings in 'F:\\ziliao\\python_kaifa\\my_c
ode\\09_\\TodoList\\migrations\\alembic.ini' before proceeding.
# python manage.py db migrate:当数据库代码有所更改时,执行这个代码,在目录中生成一个新的文件,打开文件,里面记录所更改的内容。def upgrade()是其中一个函数,执行python manage.py db upgrade代码,就是提交到数据库
(base) F:\ziliao\python_kaifa\my_code\09_\TodoList>python manage.py db migrate
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'roles'
INFO [alembic.autogenerate.compare] Detected added table 'users'
Generating F:\ziliao\python_kaifa\my_code\09_\TodoList\migrations\versions\7c280d603bcf_
.py ... done
(base) F:\ziliao\python_kaifa\my_code\09_\TodoList>python manage.py db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> 7c280d603bcf, empty message
### 在交互式环境中添加用户和用户角色并提交数据库:
(base) F:\ziliao\python_kaifa\my_code\09_\TodoList>python manage.py shell
In [1]: role=Role(name='管理员')
In [2]: user=User(username="daliu")
In [3]: user.password = 'daliu'
In [4]: user.role=role
In [5]: db.session.add(role)
In [6]: db.session.add(user)
In [7]: db.session.commit()
In [8]: print(role.users)
[<User: daliu>]
In [9]: print(user.role.name)
管理员
In [10]:
python manage.py db migrate:当数据库代码有所更改时,执行这个代码,在目录中生成一个新的文件,打开文件,里面记录所更改的内容。def upgrade()是其中一个函数,执行python manage.py db upgrade代码,就是提交到数据库

提交github:
(base) F:\ziliao\python_kaifa\my_code\09_\TodoList>git add *
(base) F:\ziliao\python_kaifa\my_code\09_\TodoList>git commit -m "add user model"
[master d42f058] add user model
15 files changed, 349 insertions(+), 16 deletions(-)
create mode 100644 .idea/.gitignore
create mode 100644 .idea/dataSources.xml
create mode 100644 app/__pycache__/models.cpython-37.pyc
create mode 100644 data-dev.sqlite
create mode 100644 migrations/README
create mode 100644 migrations/__pycache__/env.cpython-37.pyc
create mode 100644 migrations/alembic.ini
create mode 100644 migrations/env.py
create mode 100644 migrations/script.py.mako
create mode 100644 migrations/versions/7c280d603bcf_.py
create mode 100644 migrations/versions/__pycache__/7c280d603bcf_.cpython-37.pyc
create mode 100644 tests/test_user_model.py
(base) F:\ziliao\python_kaifa\my_code\09_\TodoList>git push origin master
fatal: HttpRequestException encountered.
发送请求时出错。
Username for 'https://github.com': error: failed to execute prompt script (exit code 1)
Username for 'https://github.com': daliu-daliu
Password for 'https://daliu-daliu@github.com':
Enumerating objects: 32, done.
Counting objects: 100% (32/32), done.
Delta compression using up to 4 threads
Compressing objects: 100% (22/22), done.
Writing objects: 100% (24/24), 9.77 KiB | 1.95 MiB/s, done.
Total 24 (delta 6), reused 0 (delta 0)
remote: Resolving deltas: 100% (6/6), completed with 6 local objects.
To https://github.com/daliu-daliu/TodoList.git
e8345aa..d42f058 master -> master
(base) F:\ziliao\python_kaifa\my_code\09_\TodoList>
(3)测试代码
# tests/test_user_model.py
import unittest
from app.models import User
class UserModelTestCase(unittest.TestCase):
""" 用户数据库模型测试 """
def test_password_setter(self):
"""测试新建的用户密码是否为空?"""
u = User(password='cat') #实例化一个用户对象,我们传进去了一个密码
self.assertTrue(u.password_hash is not None) #判断这个密码在加密后是否为空,不为空则OK
def test_no_password_getter(self):
"""测试获取密码信息是否报错?"""
u = User(password='cat')
with self.assertRaises(AttributeError):
password =u.password # 我们穿进去一个密码cat,如果获取到的密码有属性错误,则测试用例通过。
def test_password_verification(self):
"""测试加密密码和明文密码是否验证正确?"""
u = User(password='cat')
self.assertTrue(u.verify_password('cat')) #我们传进去了一个cat密码,验证成功,则OK
self.assertFalse(u.verify_password('dog'))#在传进一个错误的密码,False则通过
def test_password_salts_are_random(self):
"""测试每次密码加密的加密字符是否不一致?"""
u = User(password='cat')
u2 = User(password='cat')
self.assertTrue(u.password_hash != u2.password_hash)

2.Flask-Login优化数据库模型
(1)技术要点
用户登录程序后,他们的认证状态要被记录下来,这样浏览不同的页面时才能记住这个状态。Flask-Login
是个非常有用的小型扩展,**专门用来管理用户认证系统中的认证状态,**且不依赖特定的认证机制。
Flask-Login 提供了一个 UserMixin 类,包含常用方法的默认实现,且能满足大多数需求。

(2)安装插件
pip install -i https://pypi.douban.com/simple flask-login
(3)核心代码
- Flask-Login 在程序的工厂函数中初始化, 修改文件
app/__init__.py:
session_protection 属性提供不同的安全等级防止用户会话遭篡改。可以设为:
- None
- ‘basic’
- ‘strong’ : 记录客户端 IP 地址和浏览器的用户代理信息,如果发现异动就登出用户。
# app/__init__.py
from flask_login import LoginManager
# .......此处省略前面重复的代码
login_manager = LoginManager()
# session_protection 属性提供不同的安全等级防止用户会话遭篡改。
login_manager.session_protection = 'strong'
# login_view 属性设置登录页面的端点。
login_manager.login_view = 'auth.login'
def create_app(config_name='development'):
# .......
# 用户认证新加扩展
login_manager.init_app(app)
# .......
return app
app/models.py:修改 User 模型,支持用户登录, 同时Flask-Login 要求程序实现一个回调函数,使用指定的标识符加载用户。
# app/models.py
from flask_login import UserMixin
from . import login_manager
'''
Flask-Login 提供了一个 UserMixin 类,包含常用方法的默认实现,且能满足大多数需求。
1). is_authenticated 用户是否已经登录?
2). is_active 是否允许用户登录?False代表用户禁用
3). is_anonymous 是否匿名用户?
4). get_id() 返回用户的唯一标识符
'''
# Flask中一个Model子类就是数据库中的一个表。默认表名'User'.lower() ===> user
class User(UserMixin,db.Model):
"""用户"""
# .............
# 电子邮件地址email,相对于用户名而言,用户更不容易忘记自己的电子邮件地址。
email = db.Column(db.String(64), unique=True, index=True)
# 加载用户的回调函数;如果能找到用户,返回用户对象;否则返回 None 。即把用户信息存储到content_user
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
3.数据库创建
Flask-Migrate 插件提供 Alembic ( Database migration 数据迁移跟踪记录)提供的数据库升级和降
级的功能。它所能实现的效果有如 Git 管理项目代码一般。
在新数据库中创建数据表。可使用如下Bash命令创建数据表或者升级到最新修订版本:
# 初始化数据库, 创建migrations目录,存放了所有迁移脚本。只需要执行一次。
python3 manage.py db init
# 创建迁移脚本
python3 manage.py db migrate
# 更新数据库
python3 manage.py db upgrade
二、用户注册
如果新用户想成为程序的成员,必须在程序中注册,这样程序才能识别并登入用户。程序的登录页面中要
显示一个链接,把用户带到注册页面,让用户输入电子邮件地址、用户名和密码。
1.用户注册表单
注册页面使用的表单要求用户输入电子邮件地址、用户名和密码。
# app/auth/forms.py:用户注册表单
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo, ValidationError
from app.models import User
class RegisterationForm(FlaskForm):
email=StringField('电子邮箱',validators=[
DataRequired(),Length(1,64),Email()])
username=StringField('用户名',validators=[
DataRequired(), Length(1, 64),
#^:以什么开头,$:以什么结尾。\w:代表单个字母数字或者下划线。*:代表前一个字符出现0次或者多次
Regexp('^\w*$', message='用户名只能由字母数字或者下划线组成')])
password = PasswordField('密码', validators=[
DataRequired(), EqualTo('repassword', message='密码不一致')])
repassword = PasswordField('确认密码', validators=[DataRequired()])
submit = SubmitField('注册')
# 两个自定义的验证函数, 以validate_ 开头且跟着字段名的方法,这个方法和常规的验证函数一起调 用。
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
# 自定义的验证函数要想表示验证失败,可以抛出 ValidationError 异常,其参数就是错 误消息。
raise ValidationError('电子邮箱已经注册.')
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('用户名已经占用.')
2.用户注册业务逻辑
处理用户注册的过程没有什么难以理解的地方。提交注册表单,通过验证后,系统就使用用户填写的信息
在数据库中添加一个新用户。
处理这个任务的视图函数如下所示:
# app/auth/views.py:用户注册路由
# 2). 应用蓝图,管理路由
from flask_login import login_user, logout_user, login_required
from app import db
from app.auth import auth
from app.auth.forms import RegisterationForm, LoginForm
from flask import render_template, flash, redirect, url_for, session
from app.models import User, Role
from flask_login import current_user
@auth.route('/')
def index():
return render_template('auth/index.html')
# 报错解决: Method Not Allowed
@auth.route('/register', methods=['GET', 'POST'])
def register():
"""
/register:
- GET: 获取注册页面
- POST:获取注册页面提交的数据信息
1). 判断是否为POST方法提交数据, 并且数据是否通过表单验证.
2). 如果通过验证, 将表单提交的数据存储到数据库中,注册成功,跳转到登录页面.
获取表单提交的数据, 有两种方式:
*). form.data {'email': 'hello@qq.com', 'username': 'hello'}
*). form.email.data, form.username.data
:return:
"""
form = RegisterationForm()
if form.validate_on_submit():
user = User()
print(form.username.data)
print(form.username)
user.username = form.username.data
user.password = form.password.data
user.email = form.email.data
user.role = Role.query.filter_by(name="普通会员").first()
db.session.add(user)
#我们在配置中设置了自动提交数据库,这里不用进行提交数据库
flash("用户%s注册成功" % (user.username), category='success')
# return redirect('/login')
# url_for('auth.login')根据视图函数寻找对应的路由地址, /login
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)

3.用户注册前端渲染
注册页面使用的模板保存在 templates/auth/register.html 文件中。这个模板只需使用 Flask-Bootstrap 提供的 wtf.quick_form() 宏渲染表单即可。
{% extends 'bootstrap/base.html' %}
{% block title %}注册{% endblock %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block navbar %}
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('auth.logout') }}">注销</a></li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">登录</a></li>
{% endif %}
<p style="color: red">{{ get_flashed_messages() }}</p>
</ul>
{% endblock %}
{% block content %}
<h1 style="color:green">用户注册</h1>
{{ wtf.quick_form(form) }}
{% endblock %}
4.用户注册页面简易效果展示
因为我在app/__init__.py里卖弄设置了注册蓝图,以auth开头,所以这里的网址要以auth开头

注册成功跳转登陆界面:

将注册代码提交github
一般新打开一个终端,执行三步提交
(base) F:\ziliao\python_kaifa\my_code\09_\TodoList>git add *
(base) F:\ziliao\python_kaifa\my_code\09_\TodoList>git commit -m "add auth register"
[master 78d55f8] add auth register
15 files changed, 125 insertions(+), 19 deletions(-)
rewrite app/__pycache__/__init__.cpython-37.pyc (87%)
rewrite app/__pycache__/models.cpython-37.pyc (83%)
create mode 100644 app/auth/__pycache__/forms.cpython-37.pyc
rewrite app/auth/__pycache__/views.cpython-37.pyc (99%)
create mode 100644 app/templates/auth/register.html
create mode 100644 tests/__pycache__/test_user_model.cpython-37.pyc
(base) F:\ziliao\python_kaifa\my_code\09_\TodoList>git push origin master
Enumerating objects: 49, done.
Counting objects: 100% (47/47), done.
Delta compression using up to 4 threads
Compressing objects: 100% (26/26), done.
Writing objects: 100% (28/28), 9.48 KiB | 746.00 KiB/s, done.
Total 28 (delta 10), reused 0 (delta 0)
remote: Resolving deltas: 100% (10/10), completed with 10 local objects.
To https://github.com/daliu-daliu/TodoList.git
d42f058..78d55f8 master -> master
(base) F:\ziliao\python_kaifa\my_code\09_\TodoList>
三、用户登录
1.用户登录表单
用户的登录表单中包含一个用于输入电子邮件地址的文本字段、一个密码字段、提交按钮。
# app/auth/forms.py:用户登录表单
class LoginForm(FlaskForm):
"""用户登录表单"""
email = StringField('电子邮箱', validators=[DataRequired(), Length(1, 64), Email()])
password = PasswordField('密码', validators=[DataRequired()])
submit = SubmitField('登录')
2.用户登录业务逻辑
- 根据用户提交的 email 从数据库中加载用户, 判断用户存在?
- 如果存在, 调用用户对象的 verify_password() 方法,其参数是表单中填写的密码。判断用户密码是否正确?
- 如果密码正确,则调用 Flask-Login 中的 login_user() 函数实现用户登录。
login_user() 函数的参数是要登录的用户,以及可选的“记住我”布尔值,“记住我”也在表单中填写。 - 如果值为 False ,那么关闭浏览器后用户会话就过期了,所以下次用户访问时要重新登录。
- 如果值为 True ,那么会在用户浏览器中写入一个长期有效的 cookie,使用这个 cookie 可以复现用户
会话。
# app/auth/views.py
# ...........
@auth.route('/login',methods=['GET', 'POST']) #http://127.0.0.1:5000/auth/register
def login():
form = LoginForm()
if form.validate_on_submit():
#判断用户是否存在并且密码是否正确
user = User.query.filter_by(email=form.email.data).first()
if user is not None and user.verify_password(form.password.data):
# 调用 Flask-Login 中的 login_user() 函数,在用户会话中把用户标记为已登录。
# login_user() 函数的参数是要登录的用户,以及可选的“记住我”布尔值,“记住我”也在 表单中填写。
login_user(user)
flash('用户%s登陆成功'%(user.username))
#登陆成功去找auth路由的主页
return redirect(url_for('auth.index'))
else:
flash('用户%s登陆失败'%(form.email.data))
return redirect(url_for('auth.login'))
return render_template('auth/login.html', form=form)
3.用户登录前端渲染
登录页面使用的模板保存在 templates/auth/login.html 文件中。这个模板只需使用 Flask-Bootstrap 提供的 wtf.quick_form() 宏渲染表单即可。
# templates/auth/login.html
{% extends 'bootstrap/base.html' %}
{% block title %}用户登陆{% endblock %}
{% block content %}
<h1 >用户登陆</h1>
{{ wtf.quick_form(form) }}
<p style="color: red">{{ get_flashed_messages() }}</p>
{% endblock %}
4.用户登录页面简易效果展示

四、用户注销
为了登出用户,这个视图函数调用 Flask-Login 中的 logout_user() 函数,删除并重设用户会话。随后会显
示一个 Flash 消息,确认这次操作,再重定向到首页,这样登出就完成了。
# app/auth/views.py
@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('用户注销成功')
return register(url_for('auth.index'))
五、前端页面优化
下面的步骤是根据登录界面进行修改, 注册页面修改也是同样的方式。
1). 导入前端页面需要的静态资源, 如下图所示:

2). 资源文件夹中包含 login.html 和 register.html 两个文件, 拷贝文件到 templates/auth 目录,
如下图所示:

3). 查找文件中访问静态资源的 html 代码并修改静态资源:
六、Github与用户认证
命令行提交项目代码到 Github , 命令如下:
git add *
git commit -m "基于Flask的任务清单管理系统(二): 用户认证基本功能实现"
git push origin master
七、单元测试技术详解
1.web单元测试的分类
- 测试对象较独立,无需依赖于cookie之类的上下文
- 依赖于上下文
- web前端的测试。
2.测试方式
- 第一种类型只需要使用 unittest 的常规测试即可
- 第二种类型,例如对于login_required类型的endpoint,可使用 app.test_client() 返回测试客户端,同时附带上合适的数据。推荐使用flask-testing插件。同时,由于这类依赖比较常见,所以推荐将其独立成类。
- 第三种类型可使用selenium,但是编写selenium工作量比较大,且不够直观,且不够只管,建议使用其他方式或者人工测试
代码组织推荐:
- 测试时,依赖于某些数据,除非测试数据的增删改,否则建议编写数据导入函数(后续写),可以 减少工作量
八、用户邮箱验证
-
如何确认注册用户提供的信息是否正确?
常用方式是验证电子邮件地址。用户注册后,程序会立即发送一封确认邮件。新账户先被标记成待
确认状态,用户按照邮件中的说明操作后,才能证明自己可以被联系上。账户确认过程中,往往会要求
用户点击一个包含确认令牌的特殊 URL 链接。

-
确认令牌的特殊 URL 链接该如何设计?
链 接 形 式是http://www.example.com/auth/confirm/<id>, id 是用户的 id 。代表账户id确认成功。
出现的问题: id可任意指定, 从而确认任意账户。
解决方法: id 进行安全加密后得到令牌。 -
如何生成包含用户 id 的安全令牌?
itsdangerous模块中的TimedJSONWebSignatureSerializer类生成具有过期时间的JSON Web签名(JSON Web Signatures,JWS)
from itsdangerous import TimedJSONWebSignatureSerializer
# 生成具有过期时间的 `JSON Web` 签名对象,
# 1). westos是一个密钥,在 Flask 程序中可使用 SECRET_KEY 设置。
# 2). expires_in 参数设置令牌的过期时间,单位为秒。
s = TimedJSONWebSignatureSerializer('westos', expires_in = 3600)
# dumps() 方法为指定的数据生成一个加密签名,然后再对数据和签名进行序列化,生成令牌字符串。
token = s.dumps({ 'confirm': 23 })
# loads() 方法会检验签名和过期时间,如果通过,返回原始数据。否则抛出异常。
data = s.loads(token)
# data = { 'confirm': 23 }
1.邮箱验证数据库模型
- 模型中新加入了一个列
confirmed用来保存账户的确认状态。 generate_confirmation_token()方法生成一个令牌,有效期默认为一小时。confirm()方法检验令牌和检查令牌中id和已登录用户id是否匹配?
如果检验通过,则把新添加的 confirmed 属性设为 True
# app/models.py
class User(UserMixin, db.Model):
# .................
confirmed = db.Column(db.Boolean, default=False)
def generate_confirmation_token(self, expiration=3600):
"""生成一个令牌,有效期默认为一小时。"""
s = TimedJSONWebSignatureSerializer('westos', expiration)
return s.dumps({'confirm': self.id})
def confirm(self, token):
""" 检验令牌和检查令牌中id和已登录用户id是否匹配?如果检验通过,则把新添加的 confirmed 属 性设为 True"""
s = TimedJSONWebSignatureSerializer('westos')
try:
data = s.loads(token)
except:
return False
if data.get('confirm') != self.id:
return False
self.confirmed = True
db.session.add(self)
db.session.commit()
return True
由于模型中新加入了一个列用来保存账户的确认状态,因此要生成并执行一个新数据库迁移。 执行shell
命令如下:
# 对数据库(db)进行迁移(migrate)。-m选项用来添加迁移备注信息。
python3 manage.py db migrate -m "添加账户的确认状态"
# 生成了迁移脚本后,使用upgrade子命令即可更新数据库.
python3 manage.py db upgrade

2.注册与确认验证的业务逻辑
当前的 /register 路由把新用户添加到数据库中后,会重定向到 /index。在重定向之前,这个路由需要发送
确认邮件。

(1)注册与发送验证邮件的视图函数
当前的 /register 路由把新用户添加到数据库中后,会重定向到 /index。在重定向之前,这个路由需要发送
确认邮件, 确认邮件信息参考验证邮件模板。
此处如果注册成功且验证通过, 应该跳转到网站的首页。
# app/auth/views.py
@auth.route('/register', methods=['GET', 'POST'])
def register():
# ....前面吧新用户添加到数据库中
token = user.generate_confirmation_token() #生成令牌
# 发送邮件的函数Flask-Mail封装过, 可参考之前讲的内容;
# user=user, token=token是两个要传给前台confirm.html的值
send_mail(to=[user.email,], subject='请激活你的任务管理平台帐号', filename='auth/confirm', user=user, token=token)
flash('平台验证消息已经发送到你的邮箱, 请确认后登录.', category='success')
# return redirect('/login')
# url_for('auth.login')根据视图函数寻找对应的路由地址, /login
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
上述代码中send_mail() 发送的电子邮件文件模板filename=‘auth/confirm’ 如下:
电子邮件模板准备
认证蓝本使用的电子邮件模板保存在 templates/auth/confirm.html 文件中, 参考微信的模板。可在
资料包中下载 email.html 文件。 修改如下: url_for() 函数中的 _external=True 参数要求程序生成完整的 URL
#app/templates/auth/confirm.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>您好!</p>
<p>
感谢您注册任务清单管理平台:<br/>
你的邮箱注册地址为:{{user.email}}<br/>
你的注册用户名为:{{user.username}}<br/>
<!-- 当点击激活链接token时,url_for自动寻找蓝图auth下的conform路由-->
激活链接:<a href="{{url_for('auth.confirm',token=token,_external=True,)}}">{{token}}</a>
</p>
</body>
</html>
上述代码中send_mail() :发送电子邮件的业务逻辑 我们还没有写,如下:
发送电子邮件的业务逻辑
如果发送了多封测试邮件,页面停滞了几秒钟,在这个过程中浏览器就像无响应一样。为了避免处理请求
过程中不必要的延迟,我们可以把发送电子邮件的函数移到后台线程中, 实现多线程发送用户验证邮
件。
#app/auth/send_mail.py
from threading import Thread
from flask import render_template, current_app
from flask_mail import Mail, Message
def thread_task(app, mail, msg):
with app.app_context():
mail.send(msg)
def send_mail(to, subject, filename, **kwargs):
""" 发送邮件的封装
:param to: 收件人
:param subject: 邮件主题
:param filename: 邮件正文对应的html名称
:param kwargs: 关键字参数, 模版中需要的变量名
:return:
"""
#获取app现在的状态
app = current_app._get_current_object()
# 初始化mail对象, 一定要先配置邮件信息;
mail = Mail(app)
msg = Message(subject=subject,
sender=app.config['MAIL_USERNAME'],
recipients=to, )
# msg.body = info
msg.html = render_template(filename + '.html', **kwargs)
# with app.app_context(): # mail.send(msg)
thread = Thread(target=thread_task, args=(app, mail, msg))
thread.start()
return thread
在上述代码中,初始化邮件对象前一定要配置邮件相关参数,如下:
电子邮件配置信息准备
我们实在开发环境中配置的信息,如果希望在测试和其他环境中都希望用到这个配置信息的话,可以在Config类中配置。
# config.py
class DevelopmentConfig(Config):
"""
开发环境的配置信息
"""
# 启用了调试支持,服务器会在代码修改后自动重新载入,并在发生错误时提供一个相当有用的调试器。
DEBUG = True
# MAIL_SERVER = 'smtp.qq.com'
MAIL_SERVER = 'smtp.163.com'
# MAIL_PORT = 587/465
MAIL_PORT = 25
# MAIL_USE_SSL = True # 163邮箱不能打开这个参数
MAIL_USERNAME = '13679127704@163.com'
MAIL_PASSWORD = '13279443585lh' #此密码为打开POP服务的授权码,如果忘记了可以重新开启获取。
我是用13679127704@163.com发送给1104213995@qq.com
运行程序
python manage.py runsrever
打开http://127.0.0.1:5000/register
注册我的真实的qq邮箱来测试

注册成功之后,打开邮箱收到一封来自13679127704@163.com的邮件

此时点击确认链接没有效果,下面是确认账户的视图函数编写。
(2)确认账户的视图函数如下面代码所示。
Flask-Login 提供的 login_required 修饰器会保护这个路由,因此,用户点击确认邮件中的链接后,要先登录,然后才能执行这个视图函数。这个函数先检查已登录的用户是否已经确认过,如果确认过,则重定向到首页,因为很显然此时不用做什么操作。这样处理可以避免用户不小心多次点击确认令牌带来的额外工作。
# app/auth/views.py
# http://127.0.0.1:5000/confirm/chdchjefjhreufhrufrfhyre
# /confirm/chdchjefjhreufhrufrfhyry
# /confirm/chdchjefjhreufhrufrfhyrz
@auth.route('/confirm/<token>')
@login_required
def confirm(token):
"""
1. 判断账户是否验证, 如果已经验证, 跳转到主页
2. 如果没有验证, 执行验证函数,更新账户的confirmed值。
"""
if current_user.confirmed:
return redirect(url_for('todo.index'))
if current_user.confirm(token):
flash('验证邮箱通过', category='success')
else:
flash('验证连接失效', category='error')
return redirect(url_for('todo.index'))
这里编写了一个非常简单的todo.route(’/’)路由:
#app/todo/views.py
@todo.route('/')
def index():
return 'todo index'
(3)过滤未确认的账户
每个程序都可以决定用户确认账户之前可以做哪些操作。比如,允许未确认的用户登录,但只显示一个未确认页面 unconfirmed.html ,这个页面要求用户在获取权限之前先确认账户。这一步可使用 Flask 提供的 before_request 钩子完成。
对蓝图来说, before_request 钩子只能应用到属于蓝图的请求上。若想在蓝图中使用针对程序全局请求的钩子,必须使用 before_app_request 修饰器。
同时满足以下 3 个条件时, before_app_request 处理程序会拦截请求。
- 用户已登录( current_user.is_authenticated 必须返回 True )。
- 用户的账户还未确认。
- 请求的端点(使用 request.endpoint 获取)不在 auth 蓝图中。访问 auth 路由要获取权限,因为这些路由的作用是让用户确认账户或执行其他账户管理操作。
如果请求满足以上 3 个条件,则会被重定向到/auth/unconfirmed路由,显示一个确认账户相关信息的页面。
before_app_request钩子函数处理程序
也就是测试中当我们登陆了但没验证会跳转进入unconfirmed的页面:
# app/auth/views.py
@auth.before_app_request
def before_request():
"""
钩子函数, 当用户登录且未邮箱确认账户那么进入unconfirmed的页面。
request.endpoint: /login ==='auth.login'
auth中的login,register,logout.....或者static的静态文件, 都不会拦截
:return:
"""
print('request.endpoint',request.endpoint) #todo.index
if current_user.is_authenticated \
and not current_user.confirmed \
and request.endpoint[:5] != 'auth.' \
and request.endpoint != 'static':
return redirect(url_for('auth.unconfirmed'))
@auth.route('/unconfirmed')
def unconfirmed():
# 如果当前用户是匿名用户或者已经验证的用户, 则访问主页, 否则进入未验证界面;
if current_user.is_anonymous or current_user.confirmed:
return redirect(url_for('todo.index'))
token = current_user.generate_confirmation_token()
return render_template('auth/unconfirmed.html')
未确认页面的 html
# templates/auth/unconfirmed.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>尊敬的用户:</p>
你好, 你还没有确认邮箱账户, 请点击下面连接进行确认.
<a href="{{ url_for('auth.resend_confirmation', _external=True ) }}">再次发送确认
邮件</a>
</body>
</html>
重新发送邮件进行确认
显示给未确认用户的页面提供了一个链接,用于请求发送新的确认邮件,以防之前的邮件丢失。重新发送
确认邮件的视图函数代码如下:
# app/auth/views.py
@auth.route('/reconfirm')
@login_required
def resend_confirmation():
token = current_user.generate_confirmation_token()
try:
send_mail([current_user.email], '请激活你的任务管理平台帐号',
'auth/confirm', user=current_user, token=token)
except Exception as e:
print(e)
flash(str(e), category='error')
return redirect(url_for('auth.register'))
else:
flash('新的平台验证消息已经发送到你的邮箱, 请确认后登录.', category='success')
return redirect(url_for('todo.index'))
如果要使用同一个邮箱测试,在测试之前,移除数据库中我们已经注册过的邮箱
运行程序
python manage.py runsrever
打开http://127.0.0.1:5000/register
注册我的真实的qq邮箱来测试

注册成功之后,打开邮箱收到一封来自13679127704@163.com的邮件

点击链接,跳到登陆界面
就是我们呢在代码里写的,使用钩子函数拦截请求时要满足的要求:用户登录且未邮箱确认账户那么进入unconfirmed的页面。

用户登录且未邮箱确认账户将进入unconfirmed的页面。点击

此时打开邮箱,再次收到一封邮件,点击链接进行激活

激活之后,跳转todo的首页

九、提交github
如果本次代码和之前提交github的代码不是同一个目录,
需要在文件夹中删除.git文件,然后重新
git init #生成 .git文件

本文的完整代码在https://github.com/daliu-daliu/TodoList
Web开发 ------ 基于Flask的 任务清单管理系统(三): 用户资料 显示与编辑 查看ppt和项目github代码
本文详细介绍了基于Flask的任务清单管理系统用户认证部分,包括用户注册、登录、邮箱验证的过程。通过Flask-Login插件管理认证状态,使用数据库模型进行用户管理,并通过邮箱验证确认用户身份。注册时发送验证邮件,用户点击链接完成邮箱验证。登录时检查用户认证状态,未验证的用户将被重定向到确认页面。文章还涵盖了单元测试、数据库迁移和代码提交到GitHub的流程。
:用户认证&spm=1001.2101.3001.5002&articleId=104727088&d=1&t=3&u=8c898ea29a36458fb1ea9a29bc2bcf75)
958

被折叠的 条评论
为什么被折叠?



