在Python的Flask框架中验证注册用户的Email的方法


Posted in Python onSeptember 02, 2015

本教程详细介绍在用户注册过程中如何去验证他们的email地址。

工作流程上来讲,在用户注册一个新账户后会寄送一个确认信。直到用户按指示完成了邮件中的“验证”,否则他们的账户会一直处于“未验证”状态。这是大多数网络应用会采用的工作流程。

这当中很重要的一件事就是,未验证的用户有什么权限?或者说,对于你的应用,他们是有全部权限呢,还是被限制的权限呢,还是根本没有权限?对于本教程中的应用,未验证用户会在登录后进到一个页面,会提醒他们只有验证了账户才可以进入应用。

开始前说明一下,很多我们要增加的功能是Flask-用户和Flask-安全的扩展部分——问题来了,为什么不直接用这两个扩展呢?嗯,首先,这是个学习机会。同时,这俩扩展都有局限性,比如支持的数据库。要是你想用RethinkDB怎么办呢?
我们开始吧

Flask基本注册

我们将会要开始一Flask样例,这包括了用户基本注册。从这个github仓库获取代码库。一旦你创建和激活了virtualenv,运行下面的命令来快速开始:

$ pip install -r requirements.txt
$ export APP_SETTINGS="project.config.DevelopmentConfig"
$ python manage.py create_db
$ python manage.py db init
$ python manage.py db migrate
$ python manage.py create_admin
$ python manage.py runserver

在应用运行的状态下,访问http://localhost:5000/register页面,注册一个新用户。注意,注册之后应用会自动登陆,引导你进入主页面。大概看一下,然后运行代码——尤其是user这个蓝图(Blueprint是flask的一个概念)。

完成时停止服务器。

更新当前应用

模型

首先,我们来在我们project/models.py中的User模型里添加上confirmed字段:

class User(db.Model):
 
 __tablename__ = "users"
 
 id = db.Column(db.Integer, primary_key=True)
 email = db.Column(db.String, unique=True, nullable=False)
 password = db.Column(db.String, nullable=False)
 registered_on = db.Column(db.DateTime, nullable=False)
 admin = db.Column(db.Boolean, nullable=False, default=False)
 confirmed = db.Column(db.Boolean, nullable=False, default=False)
 confirmed_on = db.Column(db.DateTime, nullable=True)
 
 def __init__(self, email, password, confirmed,
   paid=False, admin=False, confirmed_on=None):
 self.email = email
 self.password = bcrypt.generate_password_hash(password)
 self.registered_on = datetime.datetime.now()
 self.admin = admin
 self.confirmed = confirmed
 self.confirmed_on = confirmed_on

注意此区域是怎样默认成“False”的。还要添加个confirmed_on字段,那是个datetime。为了用队列分析来分析registered_on和confirmed_on日期的不同,所以我想包含这个datetime。

让我们完全从头开始创建数据库并迁移吧!所以,先删除数据库dev.sqlite,以及“迁移”文件夹。

控制命令

接下来,在manage.py中,更新create_admin命令,使新的数据库字段生效:

@manager.command
def create_admin():
 """Creates the admin user."""
 db.session.add(User(
 email="ad@min.com",
 password="admin",
 admin=True,
 confirmed=True,
 confirmed_on=datetime.datetime.now())
 )
 db.session.commit()

要确保导入datetime。现在,先再一次运行下面的指令:

$ python manage.py create_db
$ python manage.py db init
$ python manage.py db migrate
$ python manage.py create_admin

register()视图函数

最后,在我们再次注册用户之前,我们需要改一下project/user/views.py中的register()视图函数……

user = User(
 email=form.email.data,
 password=form.password.data
)

改成下面的:

user = User(
 email=form.email.data,
 password=form.password.data,
 confirmed=False
)

明白了吗?思考下为什么要把confirmed默认成False。

嗯好。再运行一遍应用。转入到http://localhost:5000/register,再注册一个新用户。如果你在SQLite浏览器中打开了你的SQLite数据库,你会看到:

在Python的Flask框架中验证注册用户的Email的方法

那么,这个我新注册的用户,michael@realpython.com没有被验证。让我们验证它。

添加email验证

产生验证令牌

邮件验证应包含一个特殊URL,使得用户只需简单地点击它,即可验证他/她的账户。理想情况下,这个URL应该看起来像这样?http://yourapp.com/confirm/<id>。这里的关键是它id。在这个id中用itsdangerous包,编码用户的邮件(包含时间戳)。

创建一个叫project/token.py的文件,添加下面的代码:

# project/token.py
 
from itsdangerous import URLSafeTimedSerializer
 
from project import app
 
def generate_confirmation_token(email):
 serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
 return serializer.dumps(email, salt=app.config['SECURITY_PASSWORD_SALT'])
 
def confirm_token(token, expiration=3600):
 serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
 try:
 email = serializer.loads(
  token,
  salt=app.config['SECURITY_PASSWORD_SALT'],
  max_age=expiration
 )
 except:
 return False
 return email

所以在generate_confirmation_token()函数中,通过URLSafeTimedSerializer用在用户注册时得到的email地址生成一个令牌。那个_真实的_email在令牌中被编了码。确认令牌之后,在confirm_token()函数中,我们可以用loads()方法,它接管令牌和其过期时间——一个小时(3600秒)内有效——作为参数。只要令牌没过期,那它就会返回一个email。

在应用的配置中(BaseConfig()),确保添加SECURITY_PASSWORD_SALT:

SECURITY_PASSWORD_SALT = 'my_precious_two'

更新register()视图函数

现在再从project/user/views.py更新下register()视图函数:

@user_blueprint.route('/register', methods=['GET', 'POST'])
def register():
 form = RegisterForm(request.form)
 if form.validate_on_submit():
 user = User(
  email=form.email.data,
  password=form.password.data,
  confirmed=False
 )
 db.session.add(user)
 db.session.commit()
 
 token = generate_confirmation_token(user.email)

也是,要确保更新了这些导入模块:

from project.token import generate_confirmation_token, confirm_token

处理Email验证

接下来,添加个新的视图来解决email验证:

@user_blueprint.route('/confirm/<token>')
@login_required
def confirm_email(token):
 try:
 email = confirm_token(token)
 except:
 flash('The confirmation link is invalid or has expired.', 'danger')
 user = User.query.filter_by(email=email).first_or_404()
 if user.confirmed:
 flash('Account already confirmed. Please login.', 'success')
 else:
 user.confirmed = True
 user.confirmed_on = datetime.datetime.now()
 db.session.add(user)
 db.session.commit()
 flash('You have confirmed your account. Thanks!', 'success')
 return redirect(url_for('main.home'))

把这个添加到project/user/views.py。同样,确保更新了这些导入:

import datetime
现在我们通过令牌调用confirm_token()函数。如果成功,我们更新用户,把email_confirmed属性改成True, 设置datetime为验证发生的时间。还有,要是用户已经进行过一遍验证过程了——而且已经验证了——我们要提醒用户这点。

创建email模板

接着,添加一个基础email模板:

<p>Welcome! Thanks for signing up. Please follow this link to activate your account:</p>
<p><a href="{{ confirm_url }}">{{ confirm_url }}</a></p>
<br>
<p>Cheers!</p>
把这个在“project/templates/user”中存为activate.html。这用了一个简单叫confirm_url的变量,它会在register()视图函数中被创建。

发送邮件

通过已经安装了而且设置在project/__init__.py中的Flask-Mail的一点儿帮助来创建一个基本函数来发送邮件。

创建一个叫email.py的文件:

# project/email.py
 
from flask.ext.mail import Message
 
from project import app, mail
 
def send_email(to, subject, template):
 msg = Message(
 subject,
 recipients=[to],
 html=template,
 sender=app.config['MAIL_DEFAULT_SENDER']
 )
 mail.send(msg)

在“project”文件夹中存一下。

所以,我们只要简单地去处理收件人列表,主题,模板即可。我们会一点一点处理邮件配置的设置。

在project/user/views.py中(再次!)更新register()视图函数

@user_blueprint.route('/register', methods=['GET', 'POST'])
def register():
 form = RegisterForm(request.form)
 if form.validate_on_submit():
 user = User(
  email=form.email.data,
  password=form.password.data,
  confirmed=False
 )
 db.session.add(user)
 db.session.commit()
 
 token = generate_confirmation_token(user.email)
 confirm_url = url_for('user.confirm_email', token=token, _external=True)
 html = render_template('user/activate.html', confirm_url=confirm_url)
 subject = "Please confirm your email"
 send_email(user.email, subject, html)
 
 login_user(user)
 
 flash('A confirmation email has been sent via email.', 'success')
 return redirect(url_for("main.home"))
 
 return render_template('user/register.html', form=form)

还要添加下面的导入模块:

from project.email import send_email
我们在这里将所有的东西整合到一起。这个函数基本功能是作为控制器(直接或间接):

  • 处理最初注册,
  • 产生令牌和确认URL,
  • 寄送确认email,
  • 快速验证,
  • 用户登入,
  • 更改用户。

你注意到_external=True参数了吗?这增加了包含了hostname和port(在我们情况中,http://localhost:5000)的完整URL。

在我们测试这个之前,我们要去设定邮件设置。

邮件

在project/config.py中更新BaseConfig():

class BaseConfig(object):
 """Base configuration."""
 
 # main config
 SECRET_KEY = 'my_precious'
 SECURITY_PASSWORD_SALT = 'my_precious_two'
 DEBUG = False
 BCRYPT_LOG_ROUNDS = 13
 WTF_CSRF_ENABLED = True
 DEBUG_TB_ENABLED = False
 DEBUG_TB_INTERCEPT_REDIRECTS = False
 
 # mail settings
 MAIL_SERVER = 'smtp.googlemail.com'
 MAIL_PORT = 465
 MAIL_USE_TLS = False
 MAIL_USE_SSL = True
 
 # gmail authentication
 MAIL_USERNAME = os.environ['APP_MAIL_USERNAME']
 MAIL_PASSWORD = os.environ['APP_MAIL_PASSWORD']
 
 # mail accounts
 MAIL_DEFAULT_SENDER = 'from@example.com'

查看 official Flask-Mail documentation 以得到更多信息
如果你已经有了GMAIL账号,那你可以用它或者注册一个测试用GMAIL账户。然后把环境变量暂时设置在当前shell中:

$ export APP_MAIL_USERNAME="foo"
$ export APP_MAIL_PASSWORD="bar"

如果你GMAIL账号有两步授权, Google会屏蔽掉它。
现在开始测试!

第一个测试

打开应用,转入到 http://localhost:5000/register。然后用你能登陆的email地址注册。顺利的话,你应该会收到封email,看起来像这样:

在Python的Flask框架中验证注册用户的Email的方法

点击URL,你会转到 http://localhost:5000/。保证用户在数据库里,那‘confirmed'字段是True,有个datetime和confirmed_on字段绑定在一起。

好!

处理许可

如果你记得,在教程的开始部分,我们决定了“未验证用户可以登录但是他们会立刻被转入一个页面——我们称之为/unconfirmed路径——提醒用户需要验证账户才能使用应用”。

所以,我们要——

  • 添加/unconfirmed路径
  • 添加unconfirmed.html模板
  • 更新register()视图函数
  • 创建装饰器
  • 更新navigation.html模板
  • 添加/unconfirmed路径

添加下面的路径project/user/views.py:

@user_blueprint.route('/unconfirmed')
@login_required
def unconfirmed():
 if current_user.confirmed:
 return redirect('main.home')
 flash('Please confirm your account!', 'warning')
 return render_template('user/unconfirmed.html')

你看过类似的代码,所以我们继续。

添加unconfirmed.html模板

{% extends "_base.html" %}
 
{% block content %}
 
<h1>Welcome!</h1>
<br>
<p>You have not confirmed your account. Please check your inbox (and your spam folder) - you should have received an email with a confirmation link.</p>
<p>Didn't get the email? <a href="/">Resend</a>.</p>
 
{% endblock %}

在“project/templates/user”中,把这个存为 unconfirmed.html 。这次应该直截了当。现在,为了重新寄送验证email,就只加了一个假的URL。我们接下来会解决它的。

更新register()视图函数

现在简单地把:

return redirect(url_for("main.home"))

变成:

return redirect(url_for("user.unconfirmed"))

所以,送完验证email后,用户会进入/unconfirmed路径。

创建装饰器

# project/decorators.py
 
from functools import wraps
 
from flask import flash, redirect, url_for
from flask.ext.login import current_user
 
def check_confirmed(func):
 @wraps(func)
 def decorated_function(*args, **kwargs):
 if current_user.confirmed is False:
  flash('Please confirm your account!', 'warning')
  return redirect(url_for('user.unconfirmed'))
 return func(*args, **kwargs)
 
 return decorated_function

这里我们用一个基本函数去检查用户是否验证。如果未验证,用户会进入/unconfirmed路径。在“project”目录中,把这个存为decorators.py。

现在装饰 profile() 视图函数:

@user_blueprint.route('/profile', methods=['GET', 'POST'])
@login_required
@check_confirmed
def profile():
 ... snip ...

确保导入装饰器:

from project.decorators import check_confirmed

更新 navigation.html 模板

最后,更新navigation.html模板接下来的部分——

把:

<ul class="nav navbar-nav">
 {% if current_user.is_authenticated() %}
 <li><a href="{{ url_for('user.profile') }}">Profile</a></li>
 {% endif %}
</ul>

变成:

<ul class="nav navbar-nav">
 {% if current_user.confirmed and current_user.is_authenticated() %}
 <li><a href="{{ url_for('user.profile') }}">Profile</a></li>
 {% elif current_user.is_authenticated() %}
 <li><a href="{{ url_for('user.unconfirmed') }}">Confirm</a></li>
 {% endif %}
</ul>

又该测试了!

第二次测试

打开应用,再次用能登陆的email地址注册。(可以随便从数据库删除你之前注册的老用户,这样可以再用一遍)现在,注册完会转入http://localhost:5000/unconfirmed。

确保测试了http://localhost:5000/profile路径。这会使你转到http://localhost:5000/unconfirmed。

验证email,你就会有完整页面的权限了。快去把!

重寄送email

最后,来做重新寄送的链接。增加下面的视图函数到project/user/views.py:

@user_blueprint.route('/resend')
@login_required
def resend_confirmation():
 token = generate_confirmation_token(current_user.email)
 confirm_url = url_for('user.confirm_email', token=token, _external=True)
 html = render_template('user/activate.html', confirm_url=confirm_url)
 subject = "Please confirm your email"
 send_email(current_user.email, subject, html)
 flash('A new confirmation email has been sent.', 'success')
 return redirect(url_for('user.unconfirmed'))

现在更新unconfirmed.html 模板:

{% extends "_base.html" %}
 
{% block content %}
 
<h1>Welcome!</h1>
<br>
<p>You have not confirmed your account. Please check your inbox (and your spam folder) - you should have received an email with a confirmation link.</p>
<p>Didn't get the email? <a href="{{ url_for('user.resend_confirmation') }}">Resend</a>.</p>
 
{% endblock %}

第三次测试

你知道这次的演练内容了,这次保证去重新寄送一个新的确认email,测试链接。应该没问题。

最后,如果你给自己发好几个验证信,会发生什么? 每封都有效吗?测试一下。注册个新用户,寄送一些新的验证信。验证第一封试试。它可以吗?它应该可以。这样行吗?你是否认为要是新的寄出,其他email应该失效呢?

对这种事做些调查。测试下你用的其他网页应用。它们是怎么处理这种行为的?

更新测试套件

好的。这是为了主要功能。我们更新当前测试套件怎么样?因为它,嗯,有问题。

运行测试:

$ python manage.py test

你会看到下面的错误:

TypeError: __init__() takes at least 4 arguments (3 given)

要去改正它,只需要在project/util.py中更新setUp()方法:

def setUp(self):
 db.create_all()
 user = User(email="ad@min.com", password="admin_user", confirmed=False)
 db.session.add(user)
 db.session.commit()

测试之前,在tests/test_models.py中,注释一下test_user_registration()测试,因为我们不想为了这个测试真 的发送一封邮件。

现在再测试一下。应该全能通过!

结论

还可以做更多的:

  • 富文本 vs. 纯文本email——应该都寄出。
  • 重设定密码email——当用户忘记密码时,这些应该被寄出。
  • 用户管理——应该允许用户更新email和密码,当email变了的话,应该重新验证一遍。
  • 测试——需要写更多测试来涉及更多新功能,包含用mock/patch更新的test_user_registration(),来防止实际email被发送。
Python 相关文章推荐
Scrapy的简单使用教程
Oct 24 Python
Python如何生成树形图案
Jan 03 Python
用Python下载一个网页保存为本地的HTML文件实例
May 21 Python
Python利用heapq实现一个优先级队列的方法
Feb 03 Python
实例讲解Python中整数的最大值输出
Mar 17 Python
Python MongoDB 插入数据时已存在则不执行,不存在则插入的解决方法
Sep 24 Python
python Pillow图像处理方法汇总
Oct 16 Python
简单了解python中的f.b.u.r函数
Nov 02 Python
Python 异步协程函数原理及实例详解
Nov 13 Python
python使用正则表达式去除中文文本多余空格,保留英文之间空格方法详解
Feb 11 Python
解决Python pip 自动更新升级失败的问题
Feb 21 Python
python GUI库图形界面开发之PyQt5图片显示控件QPixmap详细使用方法与实例
Feb 27 Python
Python实现身份证号码解析
Sep 01 #Python
实例Python处理XML文件的方法
Aug 31 #Python
通过实例浅析Python对比C语言的编程思想差异
Aug 30 #Python
使用Python脚本将文字转换为图片的实例分享
Aug 29 #Python
Python中常见的数据类型小结
Aug 29 #Python
深入解析Python中的lambda表达式的用法
Aug 28 #Python
两个使用Python脚本操作文件的小示例分享
Aug 27 #Python
You might like
在PHP中利用XML技术构造远程服务(上)
2006/10/09 PHP
php注入实例
2006/10/09 PHP
PHP函数常用用法小结
2010/02/08 PHP
《PHP编程最快明白》第四讲:日期、表单接收、session、cookie
2010/11/01 PHP
php+mysql+ajax 局部刷新点赞/取消点赞功能(每个账号只点赞一次)
2020/07/24 PHP
php redis setnx分布式锁简单原理解析
2020/10/23 PHP
javascript 获取图片颜色
2009/04/05 Javascript
前端开发的开始---基于面向对象的Ajax类
2010/09/17 Javascript
Colortip基于jquery的信息提示框插件在IE6下面的显示问题修正方法
2010/12/06 Javascript
JavaScript(js)设置默认输入焦点(focus)
2012/12/28 Javascript
jquery选择器使用详解
2014/04/08 Javascript
js+ajax实现获取文件大小的方法
2015/12/08 Javascript
Angularjs使用指令做表单校验的方法
2017/03/31 Javascript
微信小程序 实现列表项滑动显示删除按钮的功能
2017/04/13 Javascript
Extjs 中的 Treepanel 实现菜单级联选中效果及实例代码
2017/08/22 Javascript
es6在react中的应用代码解析
2017/11/08 Javascript
微信小程序提取公用函数到util.js及使用方法示例
2019/01/10 Javascript
jquery获取file表单选择文件的路径、名字、大小、类型
2019/01/18 jQuery
如何在vue里面优雅的解决跨域(路由冲突问题)
2019/01/20 Javascript
浅谈发布订阅模式与观察者模式
2019/04/09 Javascript
微信JS-SDK实现微信会员卡功能(给用户微信卡包里发送会员卡)
2019/07/25 Javascript
[01:20]DOTA2 2017国际邀请赛冠军之路无止竞
2017/06/19 DOTA
Python中使用ElementTree解析XML示例
2015/06/02 Python
idea创建springMVC框架和配置小文件的教程图解
2018/09/18 Python
在PyTorch中Tensor的查找和筛选例子
2019/08/18 Python
Opencv python 图片生成视频的方法示例
2020/11/18 Python
python3中数组逆序输出方法
2020/12/01 Python
饿了么订餐官网:外卖、网上订餐
2019/06/28 全球购物
夫妻忠诚协议范文
2014/11/16 职场文书
辩论赛开场白大全(主持人+辩手)
2015/05/29 职场文书
学习焦裕禄观后感
2015/06/09 职场文书
干部考核工作总结
2015/08/12 职场文书
感恩老师主题班会
2015/08/12 职场文书
2016年大学生就业指导课心得体会
2015/10/09 职场文书
python使用tkinter实现透明窗体上绘制随机出现的小球(实例代码)
2021/05/17 Python
利用nginx搭建RTMP视频点播、直播、HLS服务器
2022/05/25 Servers