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

本教程详细介绍在用户注册过程中如何去验证他们的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数据库,你会看到:

那么,这个我新注册的用户,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,看起来像这样:

点击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被发送。
(0)

相关推荐

  • 操作Windows注册表的简单的Python程序制作教程

    通过Python操作注册表有两种方式,第一种是通过Python的内置模块 _winreg:另一种方式就是Win32 Extension For Python 的win32api模块,但是需要进行额外的安装.这里主要给出一些_winreg和win32api的Demo代码. 1. _winrg 可以参考官方的参考文档: http://docs.python.org/library/_winreg.html http://www.python.org/doc/2.6.2/library/_winreg

  • 用Python实现web端用户登录和注册功能的教程

    用户管理是绝大部分Web网站都需要解决的问题.用户管理涉及到用户注册和登录. 用户注册相对简单,我们可以先通过API把用户注册这个功能实现了: _RE_MD5 = re.compile(r'^[0-9a-f]{32}$') @api @post('/api/users') def register_user(): i = ctx.request.input(name='', email='', password='') name = i.name.strip() email = i.email.

  • Python实现注册登录系统

    表单在网页中主要负责数据采集功能.一个表单有三个基本组成部分: 表单标签:这里面包含了处理表单数据所用CGI程序的URL以及数据提交到服务器的方法. 表单域:包含了文本框.密码框.隐藏域.多行文本框.复选框.单选框.下拉选择框和文件上传框等. 表单按钮:包括提交按钮.复位按钮和一般按钮:用于将数据传送到服务器上的CGI脚本或者取消输入,还可以用表单按钮来控制其他定义了处理脚本的处理工作.而在HTML里面,在地址栏输入一个URL然后打开,这样发送的是一个get请求,若是要使用POST请求,就需要使

  • python读取注册表中值的方法

    在Python的标准库中,_winreg.pyd可以操作Windows的注册表,另外第三方的win32库封装了大量的Windows API,使用起来也很方便.不过这里介绍的是使用_winreg操作注册表,毕竟是Python自带的标准库,无需安装第三方库. 下面的例子是通过Python获取Windows XP下已经安装的补丁号.Windows的补丁号都在"HKEY_LOCAL_MACHINE\SOFTWARE\\Microsoft\\Updates"下,通过循环下面所有的目录节点,如果找

  • python采用requests库模拟登录和抓取数据的简单示例

    如果你还在为python的各种urllib和urlibs,cookielib 头疼,或者还还在为python模拟登录和抓取数据而抓狂,那么来看看我们推荐的requests,python采集数据模拟登录必备利器! 这也是python推荐的HTTP客户端库: 本文就以一个模拟登录的例子来加以说明,至于采集大家就请自行发挥吧. 代码很简单,主要是展现python的requests库的简单至极,代码如下: s = requests.session() data = {'user':'用户名','pass

  • 基于python脚本实现软件的注册功能(机器码+注册码机制)

    一.前言: 目的:完成已有python图像处理工具的注册功能 功能:用户运行程序后,通过文件自动检测认证状态,如果未经认证,就需要注册.注册过程是用户将程序运行后显示的机器码(C盘的卷序号)发回给管理员,管理员对机器码加密后生成加密文件或字符串返回给用户.每次启动程序,在有注册文件的情况下,程序就会通过DES和base64解码,并与此刻获取到的C盘卷序列号比对,如果一致则运行主程序.如果注册文件解码后与卷序号不一致,就要提醒用户输入注册码,如果对新输入的解码后和重新获取的机器码一致,则通过认证,

  • Python爬虫模拟登录带验证码网站

    爬取网站时经常会遇到需要登录的问题,这是就需要用到模拟登录的相关方法.python提供了强大的url库,想做到这个并不难.这里以登录学校教务系统为例,做一个简单的例子. 首先得明白cookie的作用,cookie是某些网站为了辨别用户身份.进行session跟踪而储存在用户本地终端上的数据.因此我们需要用Cookielib模块来保持网站的cookie. 这个是要登陆的地址 http://202.115.80.153/ 和验证码地址 http://202.115.80.153/CheckCode.

  • 使用Python中的cookielib模拟登录网站

    前面简单提到了 Python 模拟登录的程序,但是没写清楚,这里再补上一个带注释的 Python 模拟登录的示例程序.简单说一下流程:先用cookielib获取cookie,再用获取到的cookie,进入需要登录的网站. # -*- coding: utf-8 -*- # !/usr/bin/python import urllib2 import urllib import cookielib import re auth_url = 'http://www.nowamagic.net/' h

  • Python使用Socket(Https)Post登录百度的实现代码

    登录百度,首先当然是先抓百度的登录包 ,由于是网页登录,最方便的自然是httpwatch了,我使用的测试账号是itiandatest1,密码是itianda,抓包结果: 复制代码 代码如下: POST /?login HTTP/1.1 Accept: image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/v

  • python使用rsa加密算法模块模拟新浪微博登录

    PC登录新浪微博时,在客户端用js预先对用户名.密码都进行了加密,而且在POST之前会GET一组参数,这也将作为POST_DATA的一部分.这样,就不能用通常的那种简单方法来模拟POST登录(比如人人网). 通过爬虫获取新浪微博数据,模拟登录是必不可少的. 1.在提交POST请求之前,需要GET获取四个参数(servertime,nonce,pubkey和rsakv),不是之前提到的只是获取简单的servertime,nonce,这里主要是由于js对用户名.密码加密方式改变了. 1.1 由于加密

随机推荐