Database
 sql >> Base de données >  >> RDS >> Database

Gestion de la confirmation par e-mail lors de l'inscription dans Flask

Ce didacticiel explique comment valider les adresses e-mail lors de l'inscription de l'utilisateur.

Mis à jour le 30/04/2015  :Ajout de la prise en charge de Python 3.

En termes de flux de travail, après qu'un utilisateur enregistre un nouveau compte, un e-mail de confirmation est envoyé. Le compte d'utilisateur est marqué comme "non confirmé" jusqu'à ce que l'utilisateur "confirme" le compte via les instructions de l'e-mail. Il s'agit d'un flux de travail simple suivi par la plupart des applications Web.

Une chose importante à prendre en compte est ce que les utilisateurs non confirmés sont autorisés à faire. En d'autres termes, ont-ils un accès complet à votre application, un accès limité/restreint ou aucun accès ? Pour l'application de ce tutoriel, les utilisateurs non confirmés peuvent se connecter mais ils sont immédiatement redirigés vers une page leur rappelant qu'ils doivent confirmer leur compte avant de pouvoir accéder à l'application.

Avant de commencer, la plupart des fonctionnalités que nous ajouterons font partie des extensions Flask-User et Flask-Security - ce qui pose la question pourquoi ne pas simplement utiliser les extensions ? Eh bien, c'est avant tout une occasion d'apprendre. De plus, ces deux extensions ont des limitations, comme les bases de données prises en charge. Et si vous vouliez utiliser RethinkDB, par exemple ?

Commençons.


Enregistrement de base du flacon

Nous allons commencer avec un passe-partout Flask qui inclut l'enregistrement utilisateur de base. Récupérez le code du référentiel. Une fois que vous avez créé et activé un environnement virtuel, exécutez les commandes suivantes pour démarrer rapidement :

$ 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

Consultez le fichier Lisez-moi pour plus d'informations.

Avec l'application en cours d'exécution, accédez à http://localhost:5000/register et enregistrez un nouvel utilisateur. Notez qu'après l'inscription, l'application vous connecte automatiquement et vous redirige vers la page principale. Jetez un coup d'œil, puis parcourez le code - en particulier le plan "utilisateur".

Tuez le serveur lorsque vous avez terminé.



Mettre à jour l'application actuelle


Modèles

Ajoutons d'abord le confirmed champ à notre User modèle dans 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

Remarquez que ce champ est par défaut sur "Faux". Nous avons également ajouté un confirmed_on champ, qui est un [datetime ] (https://realpython.com/python-datetime/). J'aime également inclure ce champ afin d'analyser la différence entre le registered_on et confirmed_on dates à l'aide d'une analyse de cohorte.

Recommençons complètement avec notre base de données et nos migrations. Alors, allez-y et supprimez la base de données, dev.sqlite , ainsi que le dossier "migrations".



Gérer la commande

Ensuite, dans manage.py , mettez à jour le create_admin commande pour prendre en compte les nouveaux champs de la base de données :

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

Assurez-vous d'importer datetime . Maintenant, continuez et exécutez à nouveau les commandes suivantes :

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


register() fonction d'affichage

Enfin, avant de pouvoir enregistrer à nouveau un utilisateur, nous devons apporter une modification rapide au register() fonction d'affichage dans project/user/views.py

Modifier :

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

À :

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

Avoir du sens ? Réfléchissez à la raison pour laquelle nous voudrions par défaut confirmed à False .

D'accord. Exécutez à nouveau l'application. Accédez à http://localhost:5000/register et enregistrez à nouveau un nouvel utilisateur. Si vous ouvrez votre base de données SQLite dans le navigateur SQLite, vous devriez voir :

Donc, le nouvel utilisateur que j'ai enregistré, [email protected] , n'est pas confirmée. Changeons cela.




Ajouter une confirmation par e-mail


Générer un jeton de confirmation

L'e-mail de confirmation doit contenir une URL unique sur laquelle un utilisateur doit simplement cliquer pour confirmer son compte. Idéalement, l'URL devrait ressembler à ceci - http://yourapp.com/confirm/<id> . La clé ici est le id . Nous allons encoder l'email de l'utilisateur (avec un horodatage) dans le id en utilisant le package itsdangerous.

Créez un fichier appelé project/token.py et ajoutez le code suivant :

# 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

Ainsi, dans le generate_confirmation_token() fonction nous utilisons le URLSafeTimedSerializer pour générer un jeton à l'aide de l'adresse e-mail obtenue lors de l'inscription de l'utilisateur. Le réel l'e-mail est encodé dans le jeton. Puis pour confirmer le jeton, dans le confirm_token() fonction, nous pouvons utiliser la fonction loads() , qui prend le jeton et l'expiration - valable une heure (3 600 secondes) - comme arguments. Tant que le jeton n'a pas expiré, il renverra un e-mail.

Assurez-vous d'ajouter le SECURITY_PASSWORD_SALT à la configuration de votre application (BaseConfig() ):

SECURITY_PASSWORD_SALT = 'my_precious_two'


Mettre à jour register() fonction d'affichage

Maintenant, mettons à jour le register() afficher à nouveau la fonction depuis 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)

Assurez-vous également de mettre à jour les importations :

from project.token import generate_confirmation_token, confirm_token


Gérer la confirmation par e-mail

Ensuite, ajoutons une nouvelle vue pour gérer l'e-mail de confirmation :

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

Ajoutez ceci à project/user/views.py . Assurez-vous également de mettre à jour les importations :

import datetime

Ici, nous appelons le confirm_token() fonction, en passant le jeton. En cas de succès, nous mettons à jour l'utilisateur, en modifiant le email_confirmed attribut à True et définir le datetime pour le moment où la confirmation a eu lieu. De plus, dans le cas où l'utilisateur a déjà suivi le processus de confirmation - et est confirmé - nous en alertons l'utilisateur.



Créer le modèle d'e-mail

Ensuite, ajoutons un modèle d'e-mail de 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>

Enregistrez-le sous activate.html dans "projet/modèles/utilisateur". Cela prend une seule variable appelée confirm_url , qui sera créé dans le register() fonction d'affichage.



Envoyer un e-mail

Créons une fonction de base pour envoyer des e-mails avec un peu d'aide de Flask-Mail, qui est déjà installé et configuré dans project/__init__.py .

Créez un fichier appelé 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)

Enregistrez-le dans le dossier "projet".

Donc, nous avons simplement besoin de passer une liste de destinataires, un sujet et un modèle. Nous traiterons des paramètres de configuration de la messagerie dans un instant.



Mettre à jour register() fonction d'affichage dans project/user/views.py (encore !)

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

Ajoutez également l'importation suivante :

from project.email import send_email

Ici, nous mettons tout en place. Cette fonction agit essentiellement comme un contrôleur (directement ou indirectement) pour l'ensemble du processus :

  • Gérer l'inscription initiale,
  • Générer un jeton et une URL de confirmation,
  • Envoyer un e-mail de confirmation,
  • Confirmation Flash,
  • Connectez l'utilisateur, et
  • Redirection de l'utilisateur.

Avez-vous remarqué le _external=True argument? Cela ajoute l'URL absolue complète qui inclut le nom d'hôte et le port (http://localhost:5000, dans notre cas.)

Avant de pouvoir tester cela, nous devons configurer nos paramètres de messagerie.



E-mail

Commencez par mettre à jour le BaseConfig() dans 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]'

Consultez la documentation officielle de Flask-Mail pour plus d'informations.

Si vous avez déjà un compte GMAIL, vous pouvez l'utiliser ou enregistrer un compte GMAIL de test. Définissez ensuite temporairement les variables d'environnement dans la session shell en cours :

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

Si votre compte GMAIL dispose d'une authentification en deux étapes, Google bloquera la tentative.

Testons maintenant !




Premier essai

Lancez l'application et accédez à http://localhost:5000/register. Inscrivez-vous ensuite avec une adresse e-mail à laquelle vous avez accès. Si tout s'est bien passé, vous devriez avoir un e-mail dans votre boîte de réception qui ressemble à ceci :

Cliquez sur l'URL et vous devriez être redirigé vers http://localhost:5000/. Assurez-vous que l'utilisateur est dans la base de données, le champ "confirmé" est True , et il y a un datetime associé au confirmed_on champ.

Génial !



Gérer les autorisations

Si vous vous souvenez, au début de ce tutoriel, nous avons décidé que "les utilisateurs non confirmés peuvent se connecter mais ils doivent être immédiatement redirigés vers une page - appelons la route /unconfirmed - rappelant aux utilisateurs qu'ils doivent confirmer leur compte avant de pouvoir accéder à l'application ».

Donc, nous devons-

  1. Ajouter le /unconfirmed itinéraire
  2. Ajouter un unconfirmed.html modèle
  3. Mettre à jour le register() fonction d'affichage
  4. Créer un décorateur
  5. Mettre à jour navigation.html modèle

Ajouter /unconfirmed itinéraire

Ajoutez la route suivante à 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')

Vous avez déjà vu un code similaire, alors passons à autre chose.



Ajouter non confirmé.html modèle

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

Enregistrez-le sous unconfirmed.html dans "projet/modèles/utilisateur". Encore une fois, tout cela devrait être simple. Pour l'instant, nous avons simplement ajouté une URL factice pour renvoyer l'e-mail de confirmation. Nous y reviendrons plus loin.



Mettre à jour le register() fonction d'affichage

Maintenant, changez simplement :

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

À :

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

Ainsi, après l'envoi de l'e-mail de confirmation, l'utilisateur est maintenant redirigé vers le /unconfirmed itinéraire.



Créer un décorateur

# 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

Ici, nous avons une fonction de base pour vérifier si un utilisateur n'est pas confirmé. Si non confirmé, l'utilisateur est redirigé vers le /unconfirmed itinéraire. Enregistrez-le sous decorators.py dans le répertoire "projet".

Maintenant, décorez le profile() fonction d'affichage :

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

Assurez-vous d'importer le décorateur :

from project.decorators import check_confirmed


Mettre à jour navigation.html modèle

Enfin, mettez à jour la partie suivante du navigation.html modèle-

Modifier :

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

À :

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

Il est temps de tester à nouveau !




Deuxième essai

Lancez l'application et enregistrez-vous à nouveau avec une adresse e-mail à laquelle vous avez accès. (N'hésitez pas à supprimer l'ancien utilisateur que vous avez enregistré auparavant de la base de données pour l'utiliser à nouveau.) Vous devriez maintenant être redirigé vers http://localhost:5000/unconfirmed après l'enregistrement.

Assurez-vous de tester la route http://localhost:5000/profile. Cela devrait vous rediriger vers http://localhost:5000/unconfirmed.

Allez-y et confirmez l'e-mail, et vous aurez accès à toutes les pages. Boum !



Renvoyer l'e-mail

Enfin, faisons fonctionner le lien de renvoi. Ajoutez la fonction d'affichage suivante à 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'))

Maintenant, mettez à jour le unconfirmed.html modèle :

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


Troisième essai

Vous connaissez le refrain. Cette fois, assurez-vous de renvoyer un nouvel e-mail de confirmation et de tester le lien. Cela devrait fonctionner.

Enfin, que se passe-t-il si vous vous envoyez quelques liens de confirmation ? Sont-ils tous valides ? Testez-le. Enregistrez un nouvel utilisateur, puis envoyez quelques nouveaux e-mails de confirmation. Essayez de confirmer avec le premier e-mail. Cela a-t-il fonctionné ? Cela devrait. Est-ce correct? Pensez-vous que ces autres e-mails doivent expirer si un nouveau est envoyé ?

Faites des recherches à ce sujet. Et testez d'autres applications Web que vous utilisez. Comment gèrent-ils un tel comportement ?



Mettre à jour la suite de tests

Très bien. Voilà pour la fonctionnalité principale. Que diriez-vous de mettre à jour la suite de tests actuelle puisqu'elle est, eh bien, cassée.

Exécutez les tests :

$ python manage.py test

Vous devriez voir l'erreur suivante :

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

Pour corriger cela, nous avons juste besoin de mettre à jour le setUp() méthode dans 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()

Maintenant, relancez les tests. Tout devrait passer !



Conclusion

Il y a clairement beaucoup plus que nous pouvons faire :

  1. E-mails enrichis ou en texte brut :nous devrions envoyer les deux.
  2. E-mail de réinitialisation du mot de passe - Ceux-ci doivent être envoyés aux utilisateurs qui ont oublié leur mot de passe.
  3. Gestion des utilisateurs :nous devons autoriser les utilisateurs à mettre à jour leurs e-mails et leurs mots de passe, et lorsqu'un e-mail est modifié, il doit être confirmé à nouveau.
  4. Tests :nous devons écrire davantage de tests pour couvrir les nouvelles fonctionnalités.

Téléchargez l'intégralité du code source depuis le référentiel Github. Commentez ci-dessous avec des questions. Découvrez la partie 2.

Joyeuses fêtes !