Web前端之Flask框架--任务清单管理系统(二:用户认证)

本文详述了使用Flask框架实现用户认证的过程,包括用户注册、登录、注销及邮箱验证的完整流程。深入探讨了数据库模型设计、Flask-Login插件应用、前后端分离表单处理、单元测试策略及前端页面优化技巧。

文章目录

一、用户认证前言

1、Flask的基本工作流程

在这里插入图片描述

2、用户发送访问请求后的基本流程

在这里插入图片描述

二、用户认证-数据库

大多数程序都要进行用户跟踪。用户连接程序时会进行身份认证,通过这一过程,让程序知道自己的身份。程序知道用户是谁后,就能提供有针对性的体验。

1、数据库模型

1)两个重要函数

Werkzeug 中的 security 模块能够很方便地实现密码散列值的计算,即更方便的对密码加密和验证。这一功能的实现只需要两个函数,分别用在注册用户和验证用户阶段。

  • 密码加密的散列值:
generate_password_hash(password, method= pbkdf2:sha1 , salt_length=8)
  • 验证密码散列值和用户输入的密码是否匹配
check_password_hash(hash, password)
2)用到的装饰器

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

装饰器表示装饰器含义
@property使能够通过获取属性的方式获取方法
@方法名.setter修改属性
@方法名.deleter删除属性
3)代码详情

设计数据库模型:
1)用户信息:User
2)用户角色信息:Role
3)用户角色:用户=1:n ,一对多关系,外键写在多的一端

app/models.py中

from app import db 
from werkzeug.security import generate_password_hash, check_password_hash
i、用户信息
class User(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=db.Column(db.String(50))
    phone=db.Column(db.String(20))
 
    # 外键写在多的一端,外键关联的是roles表中的id列
    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=pbkd2:sha1,salt_length=8):密码加密的散列值,为密码进行哈希加密
        self.password_hash=generate_password_hash(password)

    def verity_password(self,password):
        # check_password_hash(hash,password):密码散列值和用户输入的密码是否一致
        return check_password_hash(self.password_hash,password)

     def __repr__(self): return '<User % r>' % self.username
           
ii、用户角色信息
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)
iii、测试代码

自建测试代码文件

import unittest

from app.models import User

class UserModelTestCase(unittest.TestCase):
    """
    用户数据库模型测试
    """
    def test_password_setter(self):
        """测试新建的用户密码是否为空"""
        #之前设置的用户name不能为空,虽然这里没有设置username但是没有报错,是因为只有在提交给数据可时才会报错
        u=User(password='cat')
        self.assertTrue(u.password_hash is not None)

    def test_no_password_getter(self):
        """测试获取密码信息是否报错"""
        u=User(password='cat')
        #返回一个属性错误
        with self.assertRaises(AttributeError):
            password=u.password

    def test_password_vertification(self):
        """测试加密密码和明文密码是否验证正确"""
        u=User(password='cat')
        self.assertTrue(u.verity_password('cat'))
        self.assertFalse(u.verity_password('dog'))

    def taet_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是个非常有用的小型扩展,专门用来管理用户认证系统中的认证状态,且不依赖特定的认证机制

2)Flask-Login的UserMixin类

Flask-Login 提供了一个 UserMixin 类,包含常用方法的默认实现,且能满足大多数需求

方法名或属性名功能
is_authenticated用户是否已经登录
is_active是否允许用户登录?False表示用户禁用
is_anonymous是否匿名用户
get_id()返回用户的唯一标识符
3)代码详情
i、初始化flask-login

session_protection 属性提供不同的安全等级防止用户会话遭篡改,可以设为:

  • None
  • basic
  • strong:记录客户端IP地址和浏览器的用户代理信息,如果发现异常就登出用户。

Flask-Login 在程序的工厂函数中初始化,修改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'): 
	# ....... 
	# 用户认证新加扩展,将第三方插件和app关联起来
	login_manager.init_app(app) 
	# ........ 
	return app 
ii、修改User模型

支持用户登录, 同时Flask-Login 要求程序实现一个回调函数,使用指定的标识符加载用户。

app/models.py

from flask_login import UserMixin 
from . import login_manager 
"""
添加UserMixin后在终端中执行python manage.py shell
在交互环境下查看user具有哪些属性
users=User.query.all()
users
user=users[0]
dir(user) 查看用户具有哪些属性和方法
user.username  查看用户的username属性信息
这些方法在写逻辑代码时使用
print(user.is_active)
"""
class User(UserMixin,db.Model):
    """
    因为继承了UserMixin类,自动继承里面的属性和方法
    Flask-Login提供了一个UserMixin类,包含常用方法的默认实现,且能满足大多数需求
       1)is_authenticated 用户是否已经登录
       2)is_active 是否允许用户登录?False代表用户禁用
       3)is_anonymous 是否匿名用户
       4)get_id() 返回用户的唯一标识符
    """
    # .....

# 加载用户的回调函数;如果能找到用户,返回用户对象;否则返回None
# 当有用户登录时,通过查询用户id,返回此用户id对应的用户信息

# login回调函数的作用:
# 注册回调函数 当没有session_id时,通过装饰器指定的函数来读取用户到session中
# 达到前端可通过current_user去获取当前登录的用户

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

3、数据库创建

Flask-Migrate 插件提供 Alembic ( Database migration 数据迁移跟踪记录)提供的数据库升级和降级的功能。它所能实现的效果有如 Git 管理项目代码一般。

1)创建更新数据库

在新数据库中创建数据表。可使用如下Bash命令创建数据表或者升级到最新修订版本(python中在terminal中执行):


# 初始化数据库, 创建migrations目录,存放了所有迁移脚本。只需要执行一次。 
python3 manage.py db init 
# 创建迁移脚本 
python3 manage.py db migrate 
# 更新数据库 
python3 manage.py db upgrade 
2)代码中保存修改信息

若在代码中对数据库信息进行了更新

db.session.add(修改的内容)
db.session.commit()
# 提交到数据库

三、用户认证-三部曲

1、用户注册

如果新用户想成为程序的成员,必须在程序中注册,这样程序才能识别并登入用户。程序的登录页面中显示一个链接,把用户带到注册页面,让用户输入电子邮件地址、用户名和密码。

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()],
        # 给前端的标签添加下面的属性
        render_kw={
            'class': 'layui-input',
            'placeholder': '电子邮箱'
        })
    username=StringField('用户名',validators=[
        DataRequired(),Length(1,64),Regexp('^\w*$',message='用户名只能由字母数字或者下划线组成')],
        # 给前端的标签添加下面的属性
        render_kw={
            'class': 'layui-input',
            'placeholder': '用户名'
        })
    password=PasswordField('密码',validators=[
        DataRequired(),EqualTo('repassword',message='密码不一致')],
        # 给前端的标签添加下面的属性
        render_kw={
            'class': 'layui-input',
            'placeholder': '密码'
        })
    repassword=PasswordField('确认密码',validators=[
        DataRequired()],
        # 给前端的标签添加下面的属性
        render_kw={
            'class': 'layui-input',
            'placeholder': '确认密码'
        })
    submit=SubmitField('注册')

    #两个自动逸的验证函数,以validate_开头且跟着字段名的方法,这个方法和常规的验证函数一起调用
    def valdate_email(self,field):
        # field是email表单对象,field.data是email表单里提交的数据信息
        #select * from users where email='xxx' limit 1;
        if User.query.filter_by(email=field.data).first():
            raise ValidationError('邮箱地址%s已经注册!'%(field.data))

    def valdate_username(self,field):
        # field是username表单对象,field.data是username表单里提交的数据信息
        if User.query.filter_by(username=field.data).first():
            raise ValidationError('用户名%s已经注册!'%(field.data))
2)用户注册业务逻辑

处理用户注册的过程没有什么难以理解的地方。
提交注册表单,通过验证后,系统就使用用户填写的信息在数据库中添加一个新用户。

处理这个任务的视图函数如下所示:app/auth/views.py


from flask import request, redirect, url_for, flash, render_template 
from flask_login import login_user, login_required, logout_user 
from app import db 
from app.auth.forms import RegistrationForm 
from app.models import User 
from . import auth 

# 报错解决:Method not Allowed
@auth.route('/register',methods=['POST','GET'])
def register():
    """
    register:
      GET:获取注册页面
      POST:获取注册页面提交的数据信息
        1)判断是否为POST方法提交数据,并且数据是否通过表单验证
        2)如果通过验证,将表单提交的数据存储到数据库中;注册成功,跳转到登陆页面
           获取表单提交的数据有两种方式:
              i、form.data  是一个字典,通过key值获取
              ii、form.email.data   form.username.data
    """
    form=RegisterationForm()
    if form.validate_on_submit():
        user=User()
        user.username=form.username.data
        user.password=form.password.data
        user.email=form.email.data
        #用户角色Role在交互式环境中创建一个普通用户
        #role=Role(name='普通会员')
        #db.session.add(role)
        #db.session.commit()
        user.role=Role.query.filter_by(name='普通用户').first()
        db.session.add(user)
        db.session.commit()
        flash('用户%s注册成功'%(user.username),category='success')
        #url_for('auth.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() 宏渲染表单即可。

templates/auth/login.html

{% extends 'bootstrap/base.html' %}

{% 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)用户注册页面简易效果展示

在这里插入图片描述

2、用户登录

1)用户登录表单

用户的登录表单中包含一个用于输入电子邮件地址的文本字段、一个密码字段、提交按钮。

app/auth/forms.py

class LoginForm(FlaskForm):
    """用户登录表单"""
    email=StringField('电子邮箱',validators=[
        DataRequired(),Length(1,64),Email()],
        # 给前端得标签添加下面的属性
        render_kw={
            'class':'layui-input',
            'placeholder':'电子邮箱'
        })
    password=PasswordField('密码',validators=[
        DataRequired()],
        # 给前端得标签添加下面得属性信息
        render_kw={
            'class' : 'layui-input',
            'placeholder' : '密码'
        })
    submit=SubmitField('登录')
2)用户登录业务逻辑
  • 根据用户提交的 email 从数据库中加载用户, 判断用户存在?
  • 如果存在, 调用用户对象的 verify_password() 方法,其参数是表单中填写的密码。判断用户密码是
    否正确?
  • 如果密码正确,则调用 Flask-Login 中的 login_user() 函数实现用户登录。

login_user() 函数的参数是要登录的用户,以及可选的“记住我”布尔值,“记住我”也在表单中填写。

  • 如果值为 False ,那么关闭浏览器后用户会话就过期了,所以下次用户访问时要重新登录。
  • 如果值为 True ,那么会在用户浏览器中写入一个长期有效的 cookie,使用这个 cookie 可以复现用户
    会话。

app/auth/views.py

# .......
@auth.route('/login',methods=['POST','GET'])
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.verity_password(form.password.data):
            login_user(user)
            # 利用login_user函数将登录信息存入session中
            flash('用户%s登录成功'%(user.username),category='success')
            return redirect(url_for('todo.list'))
        else:
            flash('用户%s登录失败'%(form.email.data),category='error')#这里用form.email.data是因为登录信息不正确,所以需要从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' %} 
{% 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)用户登录页面简易效果展示

在这里插入图片描述

3、用户注销

为了登出用户,这个视图函数调用 Flask-Login 中logout_user() 函数,删除并重设用户会话。随后会显示一个 Flash 消息,确认这次操作,再重定向到首页,这样登出就完成了。

app/auth/views.py

@auth.route('/logout')
@login_required
#此装饰器是用来判断用户是否已经登录
#当一个函数被多个装饰器装饰时,执行的顺序时从上往下执行,被装饰的顺序是从下往上装饰
#所以这里是先进入logout路由,然后判断是否已经登录,如果已经登录则执行注销
def logout():
    logout_user()
    flash('用户注销成功',category='success')
    return redirect(url_for('auth.index'))

四、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工作量比较大,且不够直观,且不够只管,建议使用其他方式或者人工测试。
  • 测试时,依赖于某些数据,除非测试数据的增删改,否则建议编写数据导入函数(后续写),可以减少工作量。

六、前端页面优化

下面的步骤是根据登录界面进行修改, 注册页面修改也是同样的方式。

1、导入前端页面需要的静态资源,

如下图所示:
在这里插入图片描述

2、资源文件夹中包含 login.html 和 register.html 两个文件, 拷贝文件到 templates/auth 目录,

如下图所示:
在这里插入图片描述

3、查找文件中访问静态资源的 html 代码并修改静态资源:

在这里插入图片描述
修改静态资源如下:
在这里插入图片描述
访问到下图页面, 即可认为静态资源位置修改成功, 就可以接着做下面的其他操作了。

在这里插入图片描述

4、修改文templates/auth/login.html 表单提交需要的信息

如下图所示:
在这里插入图片描述

5、实现分类闪现,详细的参考消息闪现flflash的文档

当我们flash闪现信息时, 指定闪现信息的类型, 便于前端的分类展示 。

修改文件app/auth/views.py 如下所示:
在这里插入图片描述
Bootstrap提供的闪现组件警告框网址, 警告框组件通过提供一些灵活的预定义消息,为常见的用户动作提供反馈消息。
在这里插入图片描述
而闪现信息的显示在很多场景都会使用, 我们把它独立成一个文件 templates/flash.html 。

templates/flash.html,代码如下:


{% for message in get_flashed_messages(category_filter=['success']) %}    
    <div class="alert alert-warning alert-dismissible" role="alert"> 
	<button type="button" class="close" data-dismiss="alert" aria- label="Close">
	    <span aria-hidden="true">×</span> 
	</button> 
	<strong>Success! </strong> {{ message }} 
    </div> 
{% endfor %} 

{% for message in get_flashed_messages(category_filter=['error']) %} 
    <div class="alert alert-warning alert-dismissible" role="alert"> 
	<button type="button" class="close" data-dismiss="alert" aria- label="Close">
	   <span aria-hidden="true">×</span> 
	</button> 
	<strong>Warning! </strong> {{ message }} 
    </div> 
{% endfor %}

登录的前端删除自带的闪现代码, include刚才编写的前端文件 templates/auth/login.html 即可。

templates/auth/login.html,如下图所示:

<div class="login layui-anim layui-anim-up">  
    <div class="title">任务清单系统登录</div> 
    <div id="darkbannerwrap"></div> 
    {% include 'flash.html' %} 
    <form method="post" class="layui-form" action="{{ url_for('auth.login') }}"> 
	{{ form.hidden_tag() }} 
	{{ form.email() }} 
	{# <input name="username" placeholder="用户名" type="text" lay- verify="required" class="layui-input">#} 
	{# <p class="error">用户登录失败</p>#} 
	<p class="error">{{ form.email.errors[0] }}</p> 
	<hr class="hr15"> 
	{{ form.password() }}
	{# <input name="password" lay-verify="required" placeholder="密码" type="password" class="layui-input">#} 
	{# <p class="error"> 密码失败</p>#} 
	<p class="error"> {{ form.password.errors[0] }}</p> 
	<hr class="hr15"> 
	{{ form.submit() }} 
	{# <input value="登录" style="width:100%;" type="submit">#} 
	<hr class="hr20"> 
    </form> 
</div> 

6、为了让前端的 CSS 样式生效, 还需要在表单的字段域中租用前端的属性信息

修改app/auth/forms.py,如下图所示:

class LoginForm(FlaskForm):
    """用户登录表单"""
    email=StringField('电子邮箱',validators=[
        DataRequired(),Length(1,64),Email()],
        # 给前端得标签添加下面的属性
        render_kw={
            'class':'layui-input',
            'placeholder':'电子邮箱'
        })
    password=PasswordField('密码',validators=[
        DataRequired()],
        # 给前端得标签添加下面得属性信息
        render_kw={
            'class' : 'layui-input',
            'placeholder' : '密码'
        })
    submit=SubmitField('登录')

七、用户邮箱验证

1、用户认证要点

1)如何确认注册用户提供的信息是否正确?

常用方式是验证电子邮件地址。用户注册后,程序会立即发送一封确认邮件。新账户先被标记成待确认状态,用户按照邮件中的说明操作后,才能证明自己可以被联系上。账户确认过程中,往往会要求用户点击一个包含确认令牌的特殊 URL 链接。
在这里插入图片描述

2)确认令牌的特殊URL链接如何设计?
  • 链接形式是 http://www.example.com/auth/confirm/,id 是用户的 id 。代表账户id确认成功。
  • 出现的问题: id可任意指定,从而确认任意账户。
  • 解决方法: id 进行安全加密后得到令牌。
3)如何生成包含用户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 }
4)添加功能到User模型

如何将这种生成和检验令牌的功能可添加到 User 模型中。
可以按照下面的步骤实现: /register/- >/ auth/email/confirm

2、邮箱验证数据库模型

  • 模型中新加入了一个列confirmed用来保存账户的确认状态。
  • generate_confifirmation_token() 方法生成一个令牌,有效期默认为一小时。
  • confirm()方法检验令牌和检查令牌中id和已登录用户id是否匹配?
  • 如果检验通过,则把新添加的 confifirmed 属性设为 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(current_app.config['SECRET_KEY'],expiration)
        return s.dumps({'confirm':self.id})

    def confirm(self,token):
        """
        http://127.0.0.1:8000/auth/confirm/djwerqwfrfbdeorifjerf
        点击这个链接时,将confirm后面的加密盐反向解析生成一个{‘confirm’:self.id}这样的字典
        从而确认激活的账户
        """
        s=TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
        try:
            data=s.loads(token)
            # 解密的信息是一个字典:{‘confirm’:1}
        except Exception as e:
            return False
        if data.get('confirm') != self.id:
            return False
        else:
            self.confirmed=True
            db.session.add(self)
            db.session.commit()
            return True
        # 当数据库发生改变时,就需要对数据库进行更新
        # 首先进行数据迁移,然后更新。python manage.py db migrate//python manage.py db upgrade

由于模型中新加入了一个列用来保存账户的确认状态,因此要生成并执行一个新数据库迁移。 执行shell命令如下:

# 对数据库(db)进行迁移(migrate)。-m选项用来添加迁移备注信息。 
python3 manage.py db migrate -m "添加账户的确认状态" 
# 生成了迁移脚本后,使用upgrade子命令即可更新数据库. 
python3 manage.py db upgrade

3、用户注册验证邮件的业务逻辑

当前的 /register 路由把新用户添加到数据库中后,会重定向到 /index。在重定向之前,这个路由需要发送确认邮件。

1)电子邮件模板准备

认证蓝本使用的电子邮件模板为email.html文件:
在这里插入图片描述

2)电子邮件配置信息准备

config.py中

class DevelopmentConfig(Config):
    """
    开发环境配置
    """
    DEBUG=True
    #启用调试支持,服务器会在代码修改后自动载入,并在发生错误时提供一个相当有用的调试器
    """
    QQ邮箱:MAIL_PORT=465(是QQ邮箱特有的,可在QQ邮箱设置中查看) MAIL_USE_SSL=True
    163邮箱:MAIL_PORT=25(采用所有邮箱默认的端口) MAIL_USE_SSL=False(系统默认为关闭,而且这里不需要开启,因此可以不写)
    """
    MAIL_SERVER='smtp.163.com'           # 邮件服务器
    MAIL_PORT=25                         # 邮件服务器的端口
    # MAIL_USE_TLS=True qq邮箱需要,网易不需要
    MAIL_USERNAME='138xxxx0012@163.com'  # 发送者邮箱账号
    MAIL_PASSWORD='XTHNVMRBCYASMVJH'     # 授权码而不是邮箱密码
    SQLALCHEMY_DATABASE_URI='sqlite:///' + os.path.join(basedir,'data-dev.sqlite')
    #sqlite:///表示sqlite协议,开发环境下的数据存放在当前文件下的data-dev的sqlite中

4、发送电子邮件的业务逻辑

如果发送了多封测试邮件,页面停滞了几秒钟,在这个过程中浏览器就像无响应一样。为了避免处理请求过程中不必要的延迟,我们可以把发送电子邮件的函数移到后台线程中, 实现多线程发送用户验证邮件。

app/email.py中

from threading import Thread

from flask import current_app, render_template
from flask_mail import Mail, Message


def thread_task(app,mail,msg):
    """多线程发送邮件任务"""
    with app.app_context():
        # 如果发生异常,则通过try,except,来捕获异常
        try:
            result=mail.send(msg)
        except Exception as e:
            print(result)
            print(str(e))
            return False
        else:
            print(result)
            return True


def send_mail(to,subject,filename,**kwargs):
    """
    发送邮件的封装
    :param to: 收件人
    :param subject: 邮件主题
    :param filename: 邮件正文对应的html名称
    :param kwargs:关键字参数,模板中需要的变量名
    :return :
    """
    # 获取当前app,以及当前app的状态
    app=current_app._get_current_object()
    # 并将获取的app与第三方插件绑定在一起
    mail=Mail(app)
    msg=Message(subject=subject,
                sender=app.config['MAIL_USERNAME'],
                recipients=to,
                )
    # recipients 表示要发给谁
    # msg.body='anan' 文本信息可以直接添加
    msg.html=render_template(filename+'.html',**kwargs)
    # 单线程发送邮件
    # with app.app_comtext():
    #    mail.send(msg)
    # 实例化多线程
    thread=Thread(target=thread_task,args=(app,mail,msg))
    # 启动多线程任务
    thread.start()
    return thread

5、注册与确认验证的业务逻辑

1)注册与发送验证邮件的视图函数

当前的 /register 路由把新用户添加到数据库中后,会重定向到 /index。在重定向之前,这个路由需要发送确认邮件, 确认邮件信息参考验证邮件模板。

app/auth/views.py中

# ......
@auth.route('/register',methods=['POST','GET'])
def register():
    """
    register:
      GET:获取注册页面
      POST:获取注册页面提交的数据信息
        1)判断是否为POST方法提交数据,并且数据是否通过表单验证
        2)如果通过验证,将表单提交的数据存储到数据库中;注册成功,跳转到登陆页面
           获取表单提交的数据有两种方式:
              i、form.data  是一个字典,通过key值获取
              ii、form.email.data   form.username.data
    """
        # ......
    	db.session.add(user)
        db.session.commit()
        flash('用户%s注册成功'%(user.username),category='success')
        # 提交数据库之后才能赋予新用户id值,而确认令牌需要用id,所以不能延后提交
        token=user.generate_confirmation_token()
        # 接收人用列表存储
        send_mail(to=[user.email,],
                  subject='请激活你的任务管理平台账号',
                  filename='auth/confirm',
                  user=user,
                  token=token)
        flash('平台验证消息已经发送到你的邮箱,请确认后登录。',category='success')
        #return redirect('/login')
        #url_for('auth.login') 根据视图函数,寻找对应的路由地址
        return redirect(url_for('auth.login'))
    return render_template('auth/register.html',form=form)
2)确认账户的视图函数

Flask-Login 提供的 login_required 修饰器会保护这个路由,因此,用户点击确认邮件中的链接后,要先登录,然后才能执行这个视图函数。这个函数先检查已登录的用户是否已经确认过,如果确认过,则重定向到首页,因为很显然此时不用做什么操作。这样处理可以避免用户不小心多次点击确认令牌带来的额外工作。

app/auth/views.py中

@auth.route('/confirm/<token>')
@login_required # 用来确认用户是否登录,登录后才能判断当前用户是否验证
def confirm(token):
    """
    1)判断账户是否验证,如果已经验证,跳转到主页
    2)如果没有验证,执行验证函数,更新账户的confirmed值
    """
    if current_user.confirmed:
        return redirect(url_for('todo.list'))
    if current_user.confirm(token):
        flash('验证邮箱通过',category='success')
        return redirect(url_for('todo.list'))
    else:
        flash('验证连接失败',category='error')
        return redirect(url_for('auth.login'))
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 路由,显示一个确认账户相关信息的页面。

app/auth/views.py中

@auth.before_app_request
def before_request():
    """
    钩子函数:当用户已登录但是邮箱未确认账户,则进入unconfirmed页面
    request.endpoint:指通过蓝图获取的视图函数的名字(auth.login)\
    auth中的register、login、logout以及static静态文件都不会被拦截
    """
    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.list'))
    token=current_user.generate_confirmation_token()
    return render_template('auth/unconfirmed.html')

未确认页面的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>

显示给未确认用户的页面提供了一个链接,用于请求发送新的确认邮件,以防之前的邮件丢失。

重新发送确认邮件的视图函数代码如下:

@auth.route('/reconfirm')
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('auth.login'))

综上所述我们的用户邮箱验证全部完成。

习练补充:
拥有程序账户的用户有时可能需要修改账户信息。下面这些操作可使用本章介绍的技术添加到验证蓝本中。

  • 修改密码
  • 重置密码
    为避免用户忘记密码无法登入的情况,程序可以提供重设密码功能。安全起见,有必要使用类似于确认账户时用到的令牌。用户请求重设密码后,程序会向用户注册时提供的电子邮件地址发送一封包含重设令牌的邮件。用户点击邮件中的链接,令牌验证后,会显示一个用于输入新密码的表单。
  • 修改电子邮件地址
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值