本教程详细介绍在用户注册过程中如何去验证他们的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中用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/’)
@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模板:
welcome! thanks for signing up. please follow this link to activate your account:
{{ confirm_url }}
cheers!
把这个在“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 %}
welcome!
you have not confirmed your account. please check your inbox (and your spam folder) – you should have received an email with a confirmation link.
didn’t get the email? resend.
{% 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模板接下来的部分——
把:
{% if current_user.is_authenticated() %}
profile
{% endif %}
变成:
{% if current_user.confirmed and current_user.is_authenticated() %}
profile
{% elif current_user.is_authenticated() %}
confirm
{% endif %}
又该测试了!
第二次测试
打开应用,再次用能登陆的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 %}
welcome!
you have not confirmed your account. please check your inbox (and your spam folder) – you should have received an email with a confirmation link.
didn’t get the email? resend.
{% 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被发送。