sql >> Base de Datos >  >> RDS >> Database

Manejo de la confirmación por correo electrónico durante el registro en Flask

Este tutorial detalla cómo validar direcciones de correo electrónico durante el registro de usuarios.

Actualizado el 30/04/2015 :Compatibilidad con Python 3 añadida.

En términos de flujo de trabajo, después de que un usuario registra una nueva cuenta, se envía un correo electrónico de confirmación. La cuenta de usuario se marca como "no confirmada" hasta que el usuario "confirma" la cuenta a través de las instrucciones del correo electrónico. Este es un flujo de trabajo simple que siguen la mayoría de las aplicaciones web.

Una cosa importante a tener en cuenta es lo que pueden hacer los usuarios no confirmados. En otras palabras, ¿tienen acceso total a su aplicación, acceso limitado/restringido o ningún acceso? Para la aplicación de este tutorial, los usuarios no confirmados pueden iniciar sesión, pero son redirigidos inmediatamente a una página que les recuerda que deben confirmar su cuenta antes de poder acceder a la aplicación.

Antes de comenzar, la mayor parte de la funcionalidad que agregaremos es parte de las extensiones Flask-User y Flask-Security, lo que plantea la pregunta de ¿por qué no usar simplemente las extensiones? Bueno, ante todo, esta es una oportunidad para aprender. Además, ambas extensiones tienen limitaciones, como las bases de datos compatibles. ¿Qué pasaría si quisiera usar RethinkDB, por ejemplo?

Comencemos.


Matraces registro básico

Vamos a comenzar con un modelo de Flask que incluye el registro de usuario básico. Tome el código del repositorio. Una vez que haya creado y activado un virtualenv, ejecute los siguientes comandos para comenzar rápidamente:

$ 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

Consulte el archivo Léame para obtener más información.

Con la aplicación en ejecución, vaya a http://localhost:5000/register y registre un nuevo usuario. Tenga en cuenta que después del registro, la aplicación inicia sesión automáticamente y lo redirige a la página principal. Eche un vistazo, luego ejecute el código, específicamente el plano del "usuario".

Elimina el servidor cuando hayas terminado.



Actualizar la aplicación actual


Modelos

Primero, agreguemos el confirmed campo a nuestro User modelo en proyecto/modelos.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 cómo este campo está predeterminado en 'Falso'. También agregamos un confirmed_on campo, que es un [datetime ] (https://realpython.com/python-datetime/). También me gusta incluir este campo para analizar la diferencia entre registered_on y confirmed_on fechas utilizando análisis de cohortes.

Empecemos completamente de nuevo con nuestra base de datos y migraciones. Entonces, continúe y elimine la base de datos, dev.sqlite , así como la carpeta "migraciones".



Administrar comando

A continuación, dentro de manage.py , actualice el create_admin comando para tener en cuenta los nuevos campos de la base de datos:

@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()

Asegúrate de importar datetime . Ahora, continúe y ejecute los siguientes comandos nuevamente:

$ python manage.py create_db
$ python manage.py db init
$ python manage.py db migrate
$ python manage.py create_admin


register() ver función

Finalmente, antes de que podamos volver a registrar a un usuario, debemos hacer un cambio rápido en register() ver la función en project/user/views.py

Cambiar:

user = User(
    email=form.email.data,
    password=form.password.data
)

Para:

user = User(
    email=form.email.data,
    password=form.password.data,
    confirmed=False
)

¿Tener sentido? Piense en por qué querríamos confirmed por defecto a False .

Bueno. Vuelva a ejecutar la aplicación. Vaya a http://localhost:5000/register y vuelva a registrar un nuevo usuario. Si abre su base de datos SQLite en el navegador SQLite, debería ver:

Entonces, el nuevo usuario que registré, [email protected] , no está confirmado. Cambiemos eso.




Agregar correo electrónico de confirmación


Generar token de confirmación

La confirmación por correo electrónico debe contener una URL única en la que el usuario simplemente debe hacer clic para confirmar su cuenta. Idealmente, la URL debería verse así:http://yourapp.com/confirm/<id> . La clave aquí es el id . Vamos a codificar el correo electrónico del usuario (junto con una marca de tiempo) en el id usando el paquete itsdangerous.

Cree un archivo llamado project/token.py y agrega el siguiente 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

Entonces, en generate_confirmation_token() función usamos el URLSafeTimedSerializer para generar un token utilizando la dirección de correo electrónico obtenida durante el registro del usuario. El real el correo electrónico está codificado en el token. Luego, para confirmar el token, dentro de confirm_token() función, podemos usar loads() método, que toma el token y el vencimiento, válido durante una hora (3600 segundos), como argumentos. Siempre que el token no haya caducado, devolverá un correo electrónico.

Asegúrese de agregar el SECURITY_PASSWORD_SALT a la configuración de su aplicación (BaseConfig() ):

SECURITY_PASSWORD_SALT = 'my_precious_two'


Actualizar register() ver función

Ahora actualicemos el register() ver la función de nuevo desde 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)

Además, asegúrese de actualizar las importaciones:

from project.token import generate_confirmation_token, confirm_token


Gestionar la confirmación por correo electrónico

A continuación, agreguemos una nueva vista para manejar la confirmación por correo electrónico:

@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'))

Agregue esto a project/user/views.py . Además, asegúrese de actualizar las importaciones:

import datetime

Aquí, llamamos al confirm_token() función, pasando el token. Si tiene éxito, actualizamos el usuario, cambiando el email_confirmed atributo a True y configurando el datetime para cuando se produjo la confirmación. Además, en caso de que el usuario ya pasó por el proceso de confirmación, y se confirma, entonces alertamos al usuario de esto.



Cree la plantilla de correo electrónico

A continuación, agreguemos una plantilla de correo electrónico base:

<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>

Guárdalo como activate.html en “proyecto/plantillas/usuario”. Esto toma una sola variable llamada confirm_url , que se creará en el register() ver función.



Enviar correo electrónico

Vamos a crear una función básica para enviar correos electrónicos con un poco de ayuda de Flask-Mail, que ya está instalado y configurado en project/__init__.py .

Cree un archivo llamado 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)

Guárdelo en la carpeta "proyecto".

Entonces, simplemente necesitamos pasar una lista de destinatarios, un asunto y una plantilla. Nos ocuparemos de los ajustes de configuración de correo en un momento.



Actualizar register() ver la función en proyecto/usuario/views.py (¡otra vez!)

@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)

Agregue también la siguiente importación:

from project.email import send_email

Aquí, estamos juntando todo. Esta función básicamente actúa como un controlador (ya sea directa o indirectamente) para todo el proceso:

  • Manejar el registro inicial,
  • Generar token y URL de confirmación,
  • Enviar correo electrónico de confirmación,
  • Flash de confirmación,
  • Inicie sesión como usuario y
  • Redireccionar usuario.

¿Notaste el _external=True ¿argumento? Esto agrega la URL absoluta completa que incluye el nombre de host y el puerto (http://localhost:5000, en nuestro caso).

Antes de que podamos probar esto, debemos configurar nuestra configuración de correo.



Correo

Comience actualizando BaseConfig() en proyecto/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]'

Consulte la documentación oficial de Flask-Mail para obtener más información.

Si ya tiene una cuenta de GMAIL, puede usarla o registrar una cuenta de GMAIL de prueba. Luego configure las variables de entorno temporalmente en la sesión de shell actual:

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

Si su cuenta de GMAIL tiene autenticación de 2 pasos, Google bloqueará el intento.

¡Ahora vamos a probar!




Primera prueba

Inicie la aplicación y vaya a http://localhost:5000/register. Luego regístrese con una dirección de correo electrónico a la que tenga acceso. Si todo salió bien, deberías tener un correo electrónico en tu bandeja de entrada similar a este:

Haga clic en la URL y debería ser redirigido a http://localhost:5000/. Asegúrese de que el usuario esté en la base de datos, el campo 'confirmado' es True , y hay un datetime asociado con el confirmed_on campo.

¡Genial!



Manejar permisos

Si recuerda, al comienzo de este tutorial, decidimos que "los usuarios no confirmados pueden iniciar sesión, pero deben ser redirigidos inmediatamente a una página; llamemos a la ruta /unconfirmed - recordar a los usuarios que deben confirmar su cuenta antes de poder acceder a la aplicación”.

Entonces, necesitamos-

  1. Agregue el /unconfirmed ruta
  2. Agregue un unconfirmed.html plantilla
  3. Actualizar el register() ver función
  4. Crear un decorador
  5. Actualizar navigation.html plantilla

Añadir /unconfirmed ruta

Agregue la siguiente ruta 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')

Has visto código similar antes, así que sigamos adelante.



Añadir unconfirmed.html plantilla

{% 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 %}

Guarde esto como unconfirmed.html en “proyecto/plantillas/usuario”. Una vez más, todo esto debería ser sencillo. Por ahora, solo agregamos una URL ficticia para reenviar el correo electrónico de confirmación. Abordaremos esto más adelante.



Actualizar el register() ver función

Ahora simplemente cambia:

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

Para:

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

Entonces, después de enviar el correo electrónico de confirmación, el usuario ahora es redirigido a /unconfirmed ruta.



Crear un 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

Aquí tenemos una función básica para verificar si un usuario no está confirmado. Si no se confirma, se redirige al usuario a /unconfirmed ruta. Guárdelo como decorators.py en el directorio “proyecto”.

Ahora, decora el profile() ver función:

@user_blueprint.route('/profile', methods=['GET', 'POST'])
@login_required
@check_confirmed
def profile():
    # ... snip ...

Asegúrate de importar el decorador:

from project.decorators import check_confirmed


Actualizar navigation.html plantilla

Finalmente, actualice la siguiente parte de navigation.html plantilla-

Cambiar:

<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>

¡Es hora de probar de nuevo!




Segunda prueba

Inicie la aplicación y regístrese nuevamente con una dirección de correo electrónico a la que tenga acceso. (Siéntete libre de eliminar el usuario anterior que registraste primero de la base de datos para usarlo nuevamente). Ahora deberías ser redirigido a http://localhost:5000/unconfirmed después del registro.

Asegúrese de probar la ruta http://localhost:5000/profile. Esto debería redirigirte a http://localhost:5000/unconfirmed.

Continúe y confirme el correo electrónico, y tendrá acceso a todas las páginas. ¡Boom!



Reenviar correo electrónico

Finalmente, hagamos que funcione el enlace de reenvío. Agregue la siguiente función de vista 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'))

Ahora actualice el unconfirmed.html plantilla:

{% 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 %}


Tercera prueba

Ya sabes que hacer. Esta vez, asegúrese de volver a enviar un nuevo correo electrónico de confirmación y pruebe el enlace. Debería funcionar.

Finalmente, ¿qué sucede si te envías algunos enlaces de confirmación? ¿Cada uno es válido? Pruébelo. Registre un nuevo usuario y luego envíe algunos nuevos correos electrónicos de confirmación. Intenta confirmar con el primer correo electrónico. ¿Funcionó? Debería. ¿Esta bien? ¿Crees que esos otros correos electrónicos deberían caducar si se envía uno nuevo?

Investigue un poco sobre esto. Y pruebe otras aplicaciones web que utilice. ¿Cómo manejan ese comportamiento?



Actualizar conjunto de pruebas

Bien. Así que eso es todo para la funcionalidad principal. ¿Qué tal si actualizamos el conjunto de pruebas actual ya que está, bueno, roto?

Ejecute las pruebas:

$ python manage.py test

Debería ver el siguiente error:

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

Para corregir esto solo necesitamos actualizar el setUp() método en 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()

Ahora ejecute las pruebas de nuevo. ¡Todo debería pasar!



Conclusión

Claramente hay mucho más que podemos hacer:

  1. Correos electrónicos enriquecidos frente a texto sin formato:deberíamos enviar ambos.
  2. Correo electrónico de restablecimiento de contraseña:estos deben enviarse a los usuarios que hayan olvidado sus contraseñas.
  3. Administración de usuarios:debemos permitir que los usuarios actualicen sus correos electrónicos y contraseñas, y cuando se cambia un correo electrónico, debe confirmarse nuevamente.
  4. Pruebas:necesitamos escribir más pruebas para cubrir las nuevas funciones.

Descargue el código fuente completo del repositorio de Github. Comenta abajo con preguntas. Echa un vistazo a la parte 2.

¡Felices fiestas!