Este tutorial detalha como validar endereços de e-mail durante o registro do usuário.
Atualizado em 30/04/2015 :Adicionado suporte para Python 3.
Em termos de fluxo de trabalho, depois que um usuário registra uma nova conta, um e-mail de confirmação é enviado. A conta do usuário é marcada como “não confirmada” até que o usuário, bem, “confirme” a conta através das instruções no e-mail. Este é um fluxo de trabalho simples que a maioria dos aplicativos da Web segue.
Uma coisa importante a ser levada em consideração é o que os usuários não confirmados podem fazer. Em outras palavras, eles têm acesso total ao seu aplicativo, acesso limitado/restrito ou nenhum acesso? Para o aplicativo neste tutorial, os usuários não confirmados podem fazer login, mas são imediatamente redirecionados para uma página lembrando-os de que precisam confirmar sua conta antes de acessar o aplicativo.
Antes de começar, a maioria das funcionalidades que iremos adicionar faz parte das extensões Flask-User e Flask-Security - o que levanta a questão por que não usar apenas as extensões? Bem, em primeiro lugar, esta é uma oportunidade de aprender. Além disso, ambas as extensões têm limitações, como os bancos de dados suportados. E se você quisesse usar o RethinkDB, por exemplo?
Vamos começar.
Registro básico do frasco
Vamos começar com um clichê do Flask que inclui o registro básico do usuário. Pegue o código do repositório. Depois de criar e ativar um virtualenv, execute os seguintes comandos para começar rapidamente:
$ 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
Confira o leia-me para mais informações.
Com o aplicativo em execução, navegue até http://localhost:5000/register e registre um novo usuário. Observe que, após o registro, o aplicativo faz seu login automaticamente e o redireciona para a página principal. Dê uma olhada ao redor e, em seguida, execute o código - especificamente o blueprint do "usuário".
Mate o servidor quando terminar.
Atualizar o aplicativo atual
Modelos
Primeiro, vamos adicionar o
confirmed
campo para nosso User
model em project/models.py :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
Observe como este campo tem como padrão ‘False’. Também adicionamos um
confirmed_on
campo, que é um [datetime
] (https://realpython.com/python-datetime/). Eu gosto de incluir este campo também para analisar a diferença entre o registered_on
e confirmed_on
datas usando análise de coorte. Vamos recomeçar completamente com nosso banco de dados e migrações. Então, vá em frente e exclua o banco de dados, dev.sqlite , bem como a pasta “migrações”.
Gerenciar comando
Em seguida, em manage.py , atualize o
create_admin
comando para levar em consideração os novos campos do banco de dados:@manager.command
def create_admin():
"""Creates the admin user."""
db.session.add(User(
email="[email protected]",
password="admin",
admin=True,
confirmed=True,
confirmed_on=datetime.datetime.now())
)
db.session.commit()
Certifique-se de importar
datetime
. Agora, vá em frente e execute os seguintes comandos novamente:$ python manage.py create_db
$ python manage.py db init
$ python manage.py db migrate
$ python manage.py create_admin
register()
função de visualização
Finalmente, antes que possamos registrar um usuário novamente, precisamos fazer uma alteração rápida no
register()
função de visualização em project/user/views.py … Mudar:
user = User(
email=form.email.data,
password=form.password.data
)
Para:
user = User(
email=form.email.data,
password=form.password.data,
confirmed=False
)
Faz sentido? Pense em por que gostaríamos de usar o padrão
confirmed
para False
. OK. Execute o aplicativo novamente. Navegue até http://localhost:5000/register e registre um novo usuário novamente. Se você abrir seu banco de dados SQLite no navegador SQLite, deverá ver:
Então, o novo usuário que registrei,
[email protected]
, não está confirmado. Vamos mudar isso. Adicionar confirmação de e-mail
Gerar token de confirmação
A confirmação por e-mail deve conter um URL exclusivo que o usuário simplesmente precisa clicar para confirmar sua conta. Idealmente, o URL deve ser algo assim -
http://yourapp.com/confirm/<id>
. A chave aqui é o id
. Vamos codificar o e-mail do usuário (junto com um carimbo de data/hora) no id
usando o seu pacote perigoso. Crie um arquivo chamado project/token.py e adicione o seguinte código:
# 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
Então, no
generate_confirmation_token()
função usamos o URLSafeTimedSerializer
para gerar um token usando o endereço de e-mail obtido durante o registro do usuário. O real email está codificado no token. Em seguida, para confirmar o token, dentro do confirm_token()
função, podemos usar a função loads()
método, que recebe o token e a expiração - válido por uma hora (3.600 segundos) - como argumentos. Contanto que o token não tenha expirado, ele retornará um e-mail. Certifique-se de adicionar o
SECURITY_PASSWORD_SALT
para a configuração do seu aplicativo (BaseConfig()
):SECURITY_PASSWORD_SALT = 'my_precious_two'
Atualizar register()
função de visualização
Agora vamos atualizar o
register()
veja a função novamente em project/user/views.py :@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)
Além disso, certifique-se de atualizar as importações:
from project.token import generate_confirmation_token, confirm_token
Gerenciar confirmação de e-mail
Em seguida, vamos adicionar uma nova visualização para lidar com a confirmação por e-mail:
@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'))
Adicione isto a project/user/views.py . Além disso, certifique-se de atualizar as importações:
import datetime
Aqui, chamamos o
confirm_token()
função, passando o token. Se for bem-sucedido, atualizamos o usuário, alterando o email_confirmed
atributo para True
e definindo o datetime
para quando a confirmação ocorreu. Além disso, caso o usuário já tenha passado pelo processo de confirmação - e seja confirmado - então alertamos o usuário sobre isso. Crie o modelo de e-mail
Em seguida, vamos adicionar um modelo de email básico:
<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>
Salve como activate.html em “projeto/modelos/usuário”. Isso leva uma única variável chamada
confirm_url
, que será criado no register()
função de visualização. Enviar e-mail
Vamos criar uma função básica para envio de e-mails com uma pequena ajuda do Flask-Mail, que já está instalado e configurado em
project/__init__.py
. Crie um arquivo chamado 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)
Salve isso na pasta “projeto”.
Então, basta passar uma lista de destinatários, um assunto e um modelo. Nós vamos lidar com as configurações de e-mail daqui a pouco.
Atualizar register()
função de visualização em project/user/views.py (de novo!)
@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)
Adicione a seguinte importação também:
from project.email import send_email
Aqui, estamos juntando tudo. Esta função basicamente atua como um controlador (direta ou indiretamente) para todo o processo:
- Gerenciar o registro inicial,
- Gerar token e URL de confirmação,
- Enviar e-mail de confirmação,
- Confirmação do Flash,
- Faça login do usuário e
- Redirecionar usuário.
Você notou o
_external=True
argumento? Isso adiciona a URL absoluta completa que inclui o nome do host e a porta (http://localhost:5000, em nosso caso). Antes de podermos testar isso, precisamos configurar nossas configurações de e-mail.
Comece atualizando o
BaseConfig()
em project/config.py :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 = '[email protected]'
Confira a documentação oficial do Flask-Mail para mais informações.
Se você já possui uma conta GMAIL, pode usá-la ou registrar uma conta GMAIL de teste. Em seguida, defina as variáveis de ambiente temporariamente na sessão atual do shell:
$ export APP_MAIL_USERNAME="foo"
$ export APP_MAIL_PASSWORD="bar"
Se sua conta do GMAIL tiver autenticação em duas etapas, o Google bloqueará a tentativa.
Agora vamos testar!
Primeiro teste
Inicie o aplicativo e navegue até http://localhost:5000/register. Em seguida, registre-se com um endereço de e-mail ao qual você tenha acesso. Se tudo correu bem, você deve ter um e-mail em sua caixa de entrada parecido com isto:
Clique na URL e você deverá ser direcionado para http://localhost:5000/. Certifique-se de que o usuário esteja no banco de dados, o campo ‘confirmed’ é
True
, e há um datetime
associado ao confirmed_on
campo. Legal!
Gerenciar permissões
Se você se lembra, no início deste tutorial, decidimos que “usuários não confirmados podem fazer login, mas devem ser redirecionados imediatamente para uma página - vamos chamar a rota
/unconfirmed
- lembrando aos usuários que eles precisam confirmar sua conta antes de acessar o aplicativo”. Então, precisamos-
- Adicione o
/unconfirmed
rota - Adicione um unconfirmed.html modelo
- Atualize o
register()
função de visualização - Criar um decorador
- Atualizar navigation.html modelo
Adicionar /unconfirmed
rota
Adicione a seguinte rota a 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')
Você já viu código semelhante antes, então vamos seguir em frente.
Adicionar unconfirmed.html modelo
{% 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 %}
Salve como unconfirmed.html em “projeto/modelos/usuário”. Novamente, tudo isso deve ser simples. Por enquanto, apenas adicionamos um URL fictício para reenviar o e-mail de confirmação. Abordaremos isso mais abaixo.
Atualize o register()
função de visualização
Agora basta alterar:
return redirect(url_for("main.home"))
Para:
return redirect(url_for("user.unconfirmed"))
Assim, após o envio do e-mail de confirmação, o usuário agora é redirecionado para o
/unconfirmed
rota. Criar um decorador
# 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
Aqui temos uma função básica para verificar se um usuário não está confirmado. Se não confirmado, o usuário é redirecionado para o
/unconfirmed
rota. Salve como decorators.py no diretório “projeto”. Agora, decore o
profile()
função de visualização:@user_blueprint.route('/profile', methods=['GET', 'POST'])
@login_required
@check_confirmed
def profile():
# ... snip ...
Certifique-se de importar o decorador:
from project.decorators import check_confirmed
Atualizar navigation.html modelo
Por fim, atualize a seguinte parte do navigation.html modelo-
Mudar:
<ul class="nav navbar-nav">
{% if current_user.is_authenticated() %}
<li><a href="{{ url_for('user.profile') }}">Profile</a></li>
{% endif %}
</ul>
Para:
<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>
Hora de testar novamente!
Segundo teste
Inicie o aplicativo e registre-se novamente com um endereço de e-mail ao qual você tenha acesso. (Sinta-se à vontade para excluir o usuário antigo que você registrou antes do banco de dados para usar novamente.) Agora você deve ser redirecionado para http://localhost:5000/unconfirmed após o registro.
Certifique-se de testar a rota http://localhost:5000/profile. Isso deve redirecioná-lo para http://localhost:5000/unconfirmed.
Vá em frente e confirme o e-mail, e você terá acesso a todas as páginas. Estrondo!
Reenviar e-mail
Finalmente, vamos fazer o link de reenvio funcionar. Adicione a seguinte função de visualização a 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'))
Agora atualize o unconfirmed.html modelo:
{% 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 %}
Terceiro teste
Você sabe o que fazer. Desta vez, certifique-se de reenviar um novo e-mail de confirmação e testar o link. Deve funcionar.
Finalmente, o que acontece se você enviar alguns links de confirmação? Cada um é válido? Teste-o. Registre um novo usuário e envie alguns novos e-mails de confirmação. Tente confirmar com o primeiro e-mail. Funcionou? Deveria. Isso está bem? Você acha que esses outros e-mails devem expirar se um novo for enviado?
Faça alguma pesquisa sobre isso. E teste outros aplicativos da Web que você usa. Como eles lidam com tal comportamento?
Atualizar conjunto de testes
Tudo bem. Então é isso para a funcionalidade principal. Que tal atualizarmos o conjunto de testes atual, já que está quebrado.
Execute os testes:
$ python manage.py test
Você deve ver o seguinte erro:
TypeError: __init__() takes at least 4 arguments (3 given)
Para corrigir isso, basta atualizar o
setUp()
método em project/util.py :def setUp(self):
db.create_all()
user = User(email="[email protected]", password="admin_user", confirmed=False)
db.session.add(user)
db.session.commit()
Agora execute os testes novamente. Todos devem passar!
Conclusão
Há claramente muito mais que podemos fazer:
- E-mails ricos x de texto simples - devemos enviar ambos.
- E-mail de redefinição de senha - Estes devem ser enviados para usuários que esqueceram suas senhas.
- Gerenciamento de usuários - Devemos permitir que os usuários atualizem seus e-mails e senhas e, quando um e-mail for alterado, ele deverá ser confirmado novamente.
- Teste - Precisamos escrever mais testes para cobrir os novos recursos.
Baixe todo o código-fonte do repositório do Github. Comente abaixo com perguntas. Confira a parte 2.
Boas férias!