Blog
Nuestra aplicación desde la perspectiva de la base de datos, funciona correctamente. Pero, ¿por qué es importante enviar email con nuestra aplicación?, ya se que en una lección anterior te enseñé a enviar emails pero esos emails eran para implementar una especie de notificación cuando ocurriera un error al momento de enviar un formulario utilizando los métodos logger.addHandler
que lo que hacía era capturar todos los errores que ocurriesen al momento de enviar cualquier formulario que se encuentre dentro de nuestro blog.
Pero ahora, este no es el caso. Ya que configuraremos una especie de soporte por email para que los usuarios puedan tener la típica opción de recuperar su contraseña en caso de que la olviden. Esta función lo que hará será enviarles un correo electrónico que contendrá un enlace personalizado y al momento de que el usuario ingrese este tendrá acceso a un formulario donde podrá restableces su contraseña.
Asistiendo a los usuarios vía email
Vamos a comenzar a partir de este momento con esta nueva lección, esto se conoce como automatización de tareas; ya que vamos a automatizar un soporte de envío de contraseñas olvidadas para los usuarios. Esto es muy importante porque ¿te imaginas si tu sitio llegara a tener mas de 2 millones de personas y que al menos la cuarta parte de esas personas olvidaran su contraseña constantemente?, la verdad es que sería un total dolor de cabeza, así que para solucionar esto vamos a crear dentro de nuestra carpeta app un nuevo archivo llamado enviar_email.py y escribiremos allí el siguiente código:
from app.settings.config import ConexionMail import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText def contraseña_olvidada(recipiente, msj = None, motivo= 'Recuperación de contraseña'): # Configurando servidor email para contraseñas olvidadas mensaje = MIMEMultipart() # Creamos el objeto mensaje password = ConexionMail.MAIL_PASSWORD mensaje['From'] = ConexionMail.MAIL_USERNAME mensaje['To'] = recipiente mensaje['Subject'] = motivo mensaje.attach(MIMEText(msj, 'plain')) # Le decimos que el mensaje contiene solamente texto plano server = smtplib.SMTP('smtp.gmail.com: 587') server.starttls() server.login(mensaje['From'], password) server.sendmail(mensaje['From'], mensaje['To'], mensaje.as_string()) server.quit()
No hace falta explicar nuevamente este código, ya que anteriormente lo hicimos cuando configuramos el servidor para el email de notificación de un error en envios de formularios, así que ya con esto tenemos la lógica para enviar emails para cuando un usuario olvide su contraseña, ahora nos falta crear un formulario donde le solicitaremos ciertos datos al usuario antes de poder enviarles el correo de recuperación de contraseña, para ello vamos a ir a formularios.py y creamos un nuevo formulario:
#... class RecuperarContraseña(FlaskForm): email = StringField('Email', validators=[DataRequired(message='Este campo es requerido'), Email()]) submit = SubmitField('Recuperar contraseña')
Ahora tenemos que agregar dicha opción en nuestra plantilla iniciar_sesion.html:
<p> ¿Olvidaste tu contraseña? <a href="{{ url_for('recuperar_contraseña') }}">Click aquí</a> </p>
En penúltimo paso falta agregar la ruta para esta función de recuperación de contraseña, entonces vamos a dirigirnos al archivo de rutas.py:
from app.formularios import RecuperarContraseña from app.enviar_email import contraseña_olvidada @app.route('/recuperar_contraseña', methods=['GET', 'POST']) def recuperar_contraseña(): if current_user.is_authenticated: return redirect(url_for('index')) form = RecuperarContraseña() if form.validate_on_submit(): usuario = Usuario.query.filter_by(email=form.email.data).first() if usuario is None: flash('No existe ningún usuario con este correo electrónico en nuestros registros') form.email.data = "" redirect(url_for('recuperar_contraseña')) if usuario is not None: contraseña_olvidada(usuario) flash('Chequea tu email para completar la recuperación de contraseña') return redirect(url_for('login')) return render_template('recuperar_contraseña.html', titulo='Recuperar contraseña', form=form)
Al entrar en esta ruta hay que asegurarse de que el usuario no este logueado, por eso a través de la condición if lo evaluamos y si el usuario está logueado lo redirigimos a la página index ya que no tendría sentido usar esta opción si está logueado el usuario.
El último paso sería crear la plantilla para solicitarle los datos al usuario, para eso crearemos un archivo llamada recuperar_contraseña.html:
{% extends "base.html" %} {% block contenido %} <h1>{{ titulo }}</h1> <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.email.label }}<br> {{ form.email(size=64) }}<br> {% for error in form.email.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% endblock %}
Si vamos a esta vista de recuperar la contraseña olvidada, veremos que todo funciona correctamente, podemos probar introduciendo un email de el usuario que se encuentre registrado en nuestro blog y luego ir a chequearlo para comprobar de que llegue el mensaje, si hiciste todo como se plantea aquí deberías de recibir el mensaje, yo lo he comprobado y el mensaje llega al correo.
λ pip install pyjwt
Esta librería nos permitirá generar el típico enlace que es válido por cierto tiempo y es ese que nos envían la mayoría de los sitios cuando recuperamos nuestra contraseña, a este enlace se le denomina token y entonces para entender un poco mejor esto; vamos a iniciar una consola con el comando de flask shell
y vamos a comprobar como funciona:
>>> import jwt >>> token = jwt.encode({'a': 'b'}, 'flask-course', algorithm='HS256') >>> token b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhIjoiYiJ9.vAHJEeDeaFaoLtHzM6mgEc8IzSVwzpdP4fAItezj0Xs' >>> jwt.decode(token, 'flask-course', algorithm=['HS256']) {'a': 'b'}
El diccionario {'a': 'b'}
es un ejemplo de carga útil que se escribirá en el token. Para hacer que el token sea seguro, se debe proporcionar una clave secreta para usar en la creación de una firma criptográfica, y como algorithm
usamos el HS256
que es el más usado. Lo que hace que el token sea seguro es que la carga esté firmada con la clave secreta. Si alguien intentara falsificar o alterar la carga en un token, la firma se invalidaría y para generar una nueva firma se necesita la clave secreta. Cuando se verifica un token, el contenido de la carga se decodifica y se devuelve al llamante. Si la firma del token fue validada, entonces la carga útil se puede confiar como auténtica.
Cuando el usuario hace clic en el enlace enviado por correo electrónico, el token se envía de vuelta a la aplicación como parte de la url, y lo primero que hará la función de visualización que maneja esta url es verificarlo. Si la firma es válida, el usuario puede ser identificado por su id y una vez que se conoce la identidad del usuario, la aplicación puede solicitar una nueva contraseña y establecerla en su cuenta.
Vamos a dirigirnos al archivo modelos.py y agregamos el siguiente código en el modelo clase Usuario:
import jwt from time import time from app import app class Usuario(UserMixin, bdd.Model): # ... def obtener_token_contraseña(self, expiracion=600): return jwt.encode( {'recuperar_contraseña': self.id, 'expide': time() + expiracion}, app.config['SECRET_KEY'], algorithm='HS256').decode('utf-8') @staticmethod def verificar_token_contraseña(token): try: id = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])['recuperar_contraseña'] except: return return Usuario.query.get(id)
La función obtener_token_contraseña
genera el token para la contraseña, este token es generado como una cadena gracias a que lo decodificamos en utf8
con la función .decode(),
por otro lado verificar_token_contraseña
es un método estático gracias al decorador @staticmethod,
como es un método estático esto significa que se puede llamar directamente desde la clase. Un método estático es similar a un método de clase, con la única diferencia de que los métodos estáticos no reciben la clase como primer argumento, por otro lado si el token es válido, entonces el valor de recuperar_contraseña
al cargar el token es la id del usuario, por lo que puedo cargar el usuario y devolverlo.
Enviar el correo electrónico con el token de recuperación de contraseña
Vamos a generar un archivo de texto bajo el nombre de recuperar_contraseña.txt dentro de la carpeta templates, este archivo se encargará de mostrar las instrucciones en el email que enviemos:
Querido {{ usuario.username }}, Para recuperar tu contraseña haz click en el siguiente enlace: {{ url_for('resetear_contraseña', token=token, _external=True) }} Si no has sido tú el que solicitó la recuperación de contraseña por favor ignore este mensaje. Sinceramente, %Tu nombre%, Desarrollador del blog.
Ahora que tengo los tokens, puedo generar los correos electrónicos de restablecimiento de contraseña, para ello vamos a modificar la función contraseña_olvidada
que creamos anteriormente en el archivo enviar_email.py, ahora este archivo debe lucir así:
from app.settings.config import ConexionMail import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from flask import render_template from app import app def contraseña_olvidada(usuario): # Configurando servidor email para contraseñas olvidadas mensaje = MIMEMultipart() # Creamos el objeto mensaje token = usuario.obtener_token_contraseña() password = ConexionMail.MAIL_PASSWORD msj = render_template('recuperar_contraseña.txt', usuario=usuario, token=token) mensaje['From'] = ConexionMail.MAIL_USERNAME mensaje['To'] = usuario.email mensaje['Subject'] = 'Recuperar contraseña' mensaje.attach(MIMEText(msj, 'plain')) # Le decimos que el mensaje contiene solamente texto plano server = smtplib.SMTP('smtp.gmail.com: 587') server.starttls() server.login(mensaje['From'], password) server.sendmail(mensaje['From'], mensaje['To'], mensaje.as_string()) server.quit()
El cambio que le hicimos es el siguiente, le agregamos el token de seguridad y como mensaje estamos enviando ese token más el usuario que desea cambiar la contraseña, seguido del documento de texto que creamos anteriormente y que se renderizará gracias al método render_template
de flask.
En el archivo formularios.py vamos a crear el formulario de reseteo de contraseña, el cual será el formulario al que el usuario será redirigido cuando haga click en el enlace que reciba por correo electrónico:
class ResetearContraseña(FlaskForm): contraseña = PasswordField('Contraseña', validators=[DataRequired()]) contraseña2 = PasswordField( 'Repetir contraseña', validators=[DataRequired(), EqualTo('contraseña')]) submit = SubmitField('Solicitar cambio de contraseña')
Ahora vamos a ir al archivo de rutas.py y vamos a agregar una nueva ruta, para este formulario:
from app.formularios import ResetearContraseña #... @app.route('/resetear_contraseña/<token>', methods= ['GET', 'POST']) def resetear_contraseña(token): if current_user.is_authenticated: return redirect(url_for('index')) usuario = Usuario.verificar_token_contraseña(token) if not usuario: return redirect(url_for('index')) form = ResetearContraseña() if form.validate_on_submit(): usuario.def_clave(form.contraseña.data) bdd.session.commit() flash('Tu contraseña ha sido cambiada') return redirect(url_for('login')) return render_template('resetear_contraseña.html', form=form)
Acá hemos creado la ruta que recibe el token, lo que hace el correo electrónico que recibe el email es enviar dicho token hacia esta ruta, la ruta se encarga de procesarlo y si el token es válido se accederá al cambio de contraseña, pero ahora falta crear la plantilla que nos permitirá cambiar la contraseña una vez sea validado el token.
Vamos a crear es plantilla bajo el nombre de resetear_contraseña.html:
{% extends "base.html" %} {% block contenido %} <h1>Cambiar contraseña contraseña</h1> <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.contraseña.label }}<br> {{ form.contraseña(size=32) }}<br> {% for error in form.contraseña.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p> {{ form.contraseña2.label }}<br> {{ form.contraseña2(size=32) }}<br> {% for error in form.contraseña2.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% endblock %}
Emails asíncronos
Ya tenemos todo listo y configurado, pero hay un problemita que no te he comentado. ¿No notaste que desde que configuramos el primer servidor de mensajes nuestro blog tarda en procesar las solicitudes?, esto ocurre debido a que al momento de emitir la solicitud donde se tiene que enviar un email, el código se ejecuta en conjunto con el envío del email y hasta que no se termine de ejecutar la linea de código (instrucción) que envía el email las vistas no se procesarán, por lo cual el usuario presenciara una respuesta lenta por parte de nuestro blog,
Entonces como ya tenemos identificado el problema que hace que la página tarde en responder las solicitudes, la respuesta es hacer uso de los emails asíncronos, lo que hace esto es “dividir” la secuencia en que se ejecutan las solicitudes que se le hacen a nuestro blog. Entonces implementando los emails asíncronos lo que hará nuestro blog al momento de que se ejecute una solicitud donde tenga que enviarse un email es dividir dicha ejecución de enviar los emails, que es lo que hace que nuestro blog emita una respuesta lenta a la solicitud del usuario, tal como lo expliqué anteriormente.
Para configurar los emails asíncronos vamos a nuestro archivo enviar_email.py para agregar el código faltante y debería de lucir de la siguiente manera:
from app.settings.config import ConexionMail import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from flask import render_template from app import app from threading import Thread def email_asincrono(server, mensaje): server.sendmail(mensaje['From'], mensaje['To'], mensaje.as_string()) server.quit() def contraseña_olvidada(usuario): # Configurando servidor email para contraseñas olvidadas mensaje = MIMEMultipart() # Creamos el objeto mensaje token = usuario.obtener_token_contraseña() password = ConexionMail.MAIL_PASSWORD msj = render_template('recuperar_contraseña.txt', usuario=usuario, token=token) mensaje['From'] = ConexionMail.MAIL_USERNAME mensaje['To'] = usuario.email mensaje['Subject'] = 'Recuperar contraseña' mensaje.attach(MIMEText(msj, 'plain')) # Le decimos que el mensaje contiene solamente texto plano server = smtplib.SMTP('smtp.gmail.com: 587') server.starttls() server.login(mensaje['From'], password) Thread(target=email_asincrono, args=(server,mensaje)).start()
Listo, ya tenemos configurado el soporte por email para resetear contraseñas en caso de que el usuario las olvide, esto es una gran ventaja en nuestra app ya que generamos tokens que implementan seguridad en el cambio de contraseñas de nuestros usuarios y además de eso, los usuarios tienen el soporte para cambio de contraseña para resetearla cada vez que se les olvide la contraseña, vamos a dejar hasta aquí esta lección y nos vemos en la siguiente.
➡ Continúa aprendiendo con nuestro Curso de Flask – Python: