在Flask中验证注册用户的Email

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

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

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

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

我们开始吧

Flask基本注册

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

1
2
3
4
5
6
7
$ 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字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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_onconfirmed_on日期的不同,所以我想包含这个datetime

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

控制命令

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

1
2
3
4
5
6
7
8
9
10
11
@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。现在,先再一次运行下面的指令:

1
2
3
4
$ 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()视图函数……

把下面的:

1
2
3
4
user = User(
     email = form.email.data,
     password = form.password.data
)

改成下面的:

1
2
3
4
5
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的文件,添加下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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

1
SECURITY_PASSWORD_SALT = 'my_precious_two'

更新register()视图函数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
@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)

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

1
from project.token import generate_confirmation_token, confirm_token

处理Email验证

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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。同样,确保更新了这些导入:

1
import datetime

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

创建email模板

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

1
2
3
4
<p>Welcome! Thanks for signing up. Please follow this link to activate your account:< / p>
<p><a href = "{{ confirm_url }}" >{{ confirm_url }}< / a>< / p>
 
<p>Cheers!< / p>

把这个在“project/templates/user”中存为activate.html。这用了一个简单叫confirm_url的变量,它会在register()视图函数中被创建。

发送邮件

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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()视图函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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)

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

1
from project.email import send_email

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

  • 处理最初注册,
  • 产生令牌和确认URL,
  • 寄送确认email,
  • 快速验证,
  • 用户登入,
  • 更改用户。
    你注意到_external=True参数了吗?这增加了包含了hostname和port(在我们情况中,http://localhost:5000)的完整URL。

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

邮件

project/config.py中更新BaseConfig()

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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中:

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

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

第一个测试

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

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

好!

处理许可

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

所以,我们要——

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

添加/unconfirmed路径

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

1
2
3
4
5
6
7
@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模板

1
2
3
4
5
6
7
8
9
10
{% extends "_base.html" %}
 
{% block content %}
 
< h1 >Welcome!</ h1 >
 
< 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()视图函数

现在简单地把:

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

变成:

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

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

创建装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 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() 视图函数:

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

确保导入装饰器:

1
from project.decorators import check_confirmed

更新 navigation.html 模板

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

把:

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

变成:

1
2
3
4
5
6
7
< 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

1
2
3
4
5
6
7
8
9
10
@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 模板:

1
2
3
4
5
6
7
8
9
10
{% extends "_base.html" %}
 
{% block content %}
 
< h1 >Welcome!</ h1 >
 
< 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应该失效呢?

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

更新测试套件

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

运行测试:

1
$ python manage.py test

你会看到下面的错误:

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

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

1
2
3
4
5
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()测试,因为我们不想为了这个测试 的发送一封邮件。

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

结论

还可以做更多的:

  1. 富文本 vs. 纯文本email——应该都寄出。
  2. 重设定密码email——当用户忘记密码时,这些应该被寄出。
  3. 用户管理——应该允许用户更新email和密码,当email变了的话,应该重新验证一遍。
  4. 测试——需要写更多测试来涉及更多新功能,包含用mock/patch更新的test_user_registration(),来防止实际email被发送。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值