在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 相关文章推荐
Python open读写文件实现脚本
Sep 06 Python
Perl中著名的Schwartzian转换问题解决实现
Jun 02 Python
python用模块zlib压缩与解压字符串和文件的方法
Dec 16 Python
Python列表list解析操作示例【整数操作、字符操作、矩阵操作】
Jul 25 Python
Python cookbook(数据结构与算法)对切片命名清除索引的方法
Mar 13 Python
对json字符串与python字符串的不同之处详解
Dec 19 Python
Python实现判断一个整数是否为回文数算法示例
Mar 02 Python
python爬虫增加访问量的方法
Aug 22 Python
Windows下Anaconda和PyCharm的安装与使用详解
Apr 23 Python
python利用opencv保存、播放视频
Nov 02 Python
python中print格式化输出的问题
Apr 16 Python
Python内置包对JSON文件数据进行编码和解码
Apr 12 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获取本周开始日期和结束日期的方法
2015/03/09 PHP
php访问数组最后一个元素的函数end()用法
2015/03/18 PHP
PHP SPL标准库之数据结构栈(SplStack)介绍
2015/05/12 PHP
文件上传之SWFUpload插件(代码)
2015/07/30 PHP
YII Framework框架教程之日志用法详解
2016/03/14 PHP
ThinkPHP3.2框架自定义配置和加载用法示例
2018/06/14 PHP
php分享朋友圈的实现代码
2019/02/18 PHP
PHP操作路由器实现方法示例
2019/04/27 PHP
Prototype中dom对象方法汇总
2008/09/17 Javascript
基于jquery实现的可以编辑选择的下拉框的代码
2010/11/19 Javascript
JavaScript获取页面上某个元素的代码
2011/03/13 Javascript
jQuery学习笔记 更改jQuery对象
2012/09/19 Javascript
瀑布流布局并自动加载实现代码
2013/03/12 Javascript
解析ScrollPic在ie8下只滚动一遍,然后变为空白 ie6,ie7,chrome,firefox正常
2013/06/26 Javascript
jquery实现带二级菜单的导航示例
2014/04/28 Javascript
Windows系统中安装nodejs图文教程
2015/02/28 NodeJs
使用JavaScript脚本无法直接改变Asp.net中Checkbox控件的Enable属性的解决方法
2015/09/16 Javascript
jquery实时获取时间的简单实例
2017/01/26 Javascript
微信小程序开发之转发分享功能
2019/10/22 Javascript
webpack优化之代码分割与公共代码提取详解
2019/11/22 Javascript
JavaScript实现轮播图片完整代码
2020/03/07 Javascript
Electron实现应用打包、自动升级过程解析
2020/07/07 Javascript
浅谈python和C语言混编的几种方式(推荐)
2017/09/27 Python
python实现差分隐私Laplace机制详解
2019/11/25 Python
Python实现非正太分布的异常值检测方式
2019/12/09 Python
Pytorch在dataloader类中设置shuffle的随机数种子方式
2020/01/14 Python
基于python3实现倒叙字符串
2020/02/18 Python
出纳员的岗位职责
2014/02/22 职场文书
2014年师德师风学习材料
2014/05/16 职场文书
学风建设演讲稿
2014/09/12 职场文书
党员批评与自我批评发言稿
2014/10/14 职场文书
2014年底工作总结
2014/12/15 职场文书
学雷锋倡议书
2015/01/19 职场文书
Python中json.dumps()函数的使用解析
2021/05/17 Python
Java8中接口的新特性使用指南
2021/11/01 Java/Android
mysqldump进行数据备份详解
2022/07/15 MySQL