#274 Remember Me & Reset Password
- Download:
- source codeProject Files in Zip (109 KB)
- mp4Full Size H.264 Video (17.5 MB)
- m4vSmaller H.264 Video (11.9 MB)
- webmFull Size VP8 Video (14.3 MB)
- ogvFull Size Theora Video (27.6 MB)
Bien qu'il existe un certain nombre de bonnes solutions d'authentification pour applications Rails, il est tout à fait possible de créer la votre. Dans l'épisode 250 [regarder, lire], c'est ce que nous avons fait et, dans l'épisode 270 [regarder, lire], nous avons montré comment Rails 3.1 rend cela encore plus facile en fournissant has_secure_password
qui va automatiquement générer des hashs pour les mots de passe.
L'authentification que nous avons implémentée dans ces épisodes est plutôt basique. Nous allons donc ajouter, dans celui-ci, quelques fonctionnalités pour l'améliorer. Tout d'abord, nous allons ajouter une checkbox “Remember me” (Se souvenir de moi) à la page de connexion de façon à ce que les utilisateurs puissent choisir d'être connectés automatiquement. Nous ajouterons ensuite un lien “Reset Password” (Réinitialiser Mot de passe) qui permettra aux utilisateurs ayant oublié leur mot de passe de le réinitialiser. Nous implémenterons ces fonctionnalités en étendant l'application de l'épisode 270. Cette application utilise Rails 3.1 mais ce que nous allons montrer ici fonctionne tout autant avec Rails 3.0.
Ajouter une checkbox “Remember Me”
Lorsqu'un utilisateur se connecte à notre application, son id
est stocké en session. Cela se fait dans l'action create
de SessionsController
.
def create user = User.find_by_email(params[:email]) if user && user.authenticate(params[:password]) session[:user_id] = user.id redirect_to root_url, :notice => "Logged in!" else flash.now.alert = "Invalid email or password" render "new" end end
Lorsqu'un utilisateur connecté ferme son navigateur, le cookie de session est supprimé et il devra se reconnecter la prochaine fois qu'il ouvrira l'application. Nous allons remplacer ce cookie de session par un cookie permanent de façon à ce que chaque id
d'utilisateur puisse être persistant.
Le problème évident de cette solution est que les id
s sont stockés sous la forme d'entiers séquentiels. Si l'id
est stocké dans un cookie permanent, il devient facile pour un utilisateur mal intentionné de changer la valeur et de voir les données d'autres utilisateurs. Pour éviter cela, nous allons générer un token (jeton) unique et non devinable pour chaque utilisateur et stocker cette valeur dans le cookie.
Chaque utilisateur aura son propre token, qui sera stocké en base de données. Nous allons donc créer une migration qui ajoute un champ auth_token
dans la table users
et migrer la base.
$ rails g migration add_auth_token_to_users auth_token:string
Nous avons besoin d'un moyen pour générer ce token unique lorsqu'un utilisateur est créé. Nous allons donc écrire une méthode generate_token
dans le modèle User
. Cette méthode prendra un argument column
pour que nous puissions avoir, à l'avenir et si besoin est, plusieurs token.
class User < ActiveRecord::Base attr_accessible :email, :password, :password_confirmation has_secure_password validates_presence_of :password, :on => :create before_create { generate_token(:auth_token) } def generate_token(column) begin self[column] = SecureRandom.urlsafe_base64 end while User.exists?(column => self[column]) end end
Pour créer le token, nous utilisons la classe SecureRandom
d'ActiveSupport
afin de générer une chaine aléatoire. Nous vérifions si un autre utilisateur a le même token et générons un nouveau token tant que c'est le cas. Nous appelons la méthode dans un filtre before_create
de façon à ce que le token soit généré à la première sauvegarde d'un nouvel utilisateur. Si nous avions déjà des utilisateurs dans notre base, nous pourrions créer une tâche Rake pour leur fournir un token mais nous ne le ferons pas ici.
Nous allons modifier l'action create
de SessionsController
pour que lorsqu'un utilisateur se connecte, nous stockions leur token dans un cookie. Nous allons également changer l'action destroy
afin de supprimer le cookie à la déconnexion de l'utilisateur.
class SessionsController < ApplicationController def new end def create user = User.find_by_email(params[:email]) if user && user.authenticate(params[:password]) cookies.permanent[:auth_token] = user.auth_token redirect_to root_url, :notice => "Logged in!" else flash.now.alert = "Invalid email or password" render "new" end end def destroy cookies.delete(:auth_token) redirect_to root_url, :notice => "Logged out!" end end
Tout utilisateur qui s'identifie est maintenant connecté de manière permanente. Certains peuvent ne pas le vouloir, nous allons donc ajouter une checkbox pour leur permettre de choisir par eux-mêmes. Les modifications à apporter au formulaire son relativement simples. Nous devons juste mettre en place la checkbox ainsi que son label.
<h1>Log in</h1> <%= form_tag sessions_path do %> <div class="field"> <%= label_tag :email %> <%= text_field_tag :email, params[:email] %> </div> <div class="field"> <%= label_tag :password %> <%= password_field_tag :password %> </div> <div class="field"> <%= label_tag :remember_me %> <%= check_box_tag :remember_me, 1, params[:remember_me] %> </div> <div class="actions"><%= submit_tag "Log in" %></div> <% end %>
Nous pouvons maintenant modifier SessionsController
pour que le cookie permanent ne soit mis en place que lorsque la checkbox est cochée. Si elle ne l'est pas, les informations de connexion seront stockées dans un cookie de session.
def create user = User.find_by_email(params[:email]) if user && user.authenticate(params[:password]) if params[:remember_me] cookies.permanent[:auth_token] = user.auth_token else cookies[:auth_token] = user.auth_token end redirect_to root_url, :notice => "Logged in!" else flash.now.alert = "Invalid email or password" render "new" end end
Il nous reste une dernière modification à effectuer. ApplicationController
a besoin d'être changé afin de lire le token d'authentification depuis le cookie au lieu de l'id de l'utilisateur en session.
class ApplicationController < ActionController::Base protect_from_forgery private def current_user @current_user ||= User.find_by_auth_token( cookies[:auth_token]) if cookies[:auth_token] end helper_method :current_user end
Nous pouvons maintenant tester. Lorsque nous nous connectons à notre application, nous allons voir la checkbox “Remember me”. Si nous la cochons à la connexion puis fermons et ouvrons de nouveau notre navigateur, nous serons automatiquement connectés. Notre fonctionnalité 'remember me' fonctionne comme voulu.
Ajouter la fonctionnalité “Mot de passe oublié”
Nous allons maintenant voir comment permettre aux utilisateurs de réinitialiser leur mot de passe. Nous commencerons par ajouter le lien adéquat sur la page de connexion.
<h1>Log in</h1> <%= form_tag sessions_path do %> <div class="field"> <%= label_tag :email %> <%= text_field_tag :email, params[:email] %> </div> <div class="field"> <%= label_tag :password %> <%= password_field_tag :password %> </div> <p><%= link_to "forgotten password?", new_password_reset_path %></p> <div class="field"> <%= label_tag :remember_me %> <%= check_box_tag :remember_me, 1, params[:remember_me] %> </div> <div class="actions"><%= submit_tag "Log in" %></div> <% end %>
Le lien pointe vers new_password_reset_path
qui fait partie d'une ressource que nous n'avons pas encore écrite. Nous allons régler cela maintenant en créant un contrôleur PasswordResets
avec une action new
.
$ rails g controller password_resets new
Nous voulons traiter ce contrôleur comme une ressource, nous allons donc modifier le fichier de routage pour remplacer la route générée par un appel à resources
.
Auth::Application.routes.draw do get "logout" => "sessions#destroy", :as => "logout" get "login" => "sessions#new", :as => "login" get "signup" => "users#new", :as => "signup" root :to => "home#index" resources :users resources :sessions resources :password_resets end
Ce n'est pas une ressource liée à un modèle mais cela fera l'affaire.
Dans la vue de l'action new
, nous allons créer un formulaire permettant aux utilisateurs de saisir leur adresse e-mail et de demander une réinitialisation de le leur mot de passe. Le formulaire ressemble à ceci :
<h1>Reset Password</h1> <%= form_tag password_resets_path, :method => :post do %> <div class="field"> <%= label_tag :email %> <%= text_field_tag :email, params[:email] %> </div> <div class="actions"><%= submit_tag "Reset Password" %></div> <% end %>
Cette ressource n'étant pas liée à un modèle, nous utilisons ici form_tag
. Le formulaire envoie les informations en POST à l'action create
de PasswordResets
que nous allons maintenant écrire. Dedans, nous allons trouver l'utilisateur auquel appartient l'e-mail fournit et lui envoyer les instructions de réinitialisation de son mot de passe. Cela sera fait dans une nouvelle méthode send_password_reset
dans le modèle User
.
def create user = User.find_by_email(params[:email]) user.send_password_reset if user redirect_to root_url, :notice => "Email sent with password reset instructions." end
Le message d'information est affiché que l'utilisateur soit trouvé ou non. Cela rend le tout un peu plus sécurisé de façon à ce qu'un utilisateur mal intentionné ne puis pas savoir si tel ou tel utilisateur existe en base.
Nous allons maintenant écrire la méthode send_password_reset
. Dedans nous allons envoyer un e-mail contenant un token pour la requête de réinitialisation du mot de passe. Nous voulons que ce token expire après un certain temps, deux heures par exemple, de façon à ce que le lien ne soit valide que peu de temps après la requête. Pour stocker cette information, nous allons avoir besoin de quelques champs en plus dans la table des utilisateurs. Nous allons donc écrire et lancer une migration pour les ajouter.
$ rails g migration add_password_reset_to_users password_reset_token:string password_reset_sent_at:datetime
Dans send_password_reset
nous allons utiliser la méthode generate_token
, que nous avons écrite précédemment, pour créer un token de réinitialisation de mot de passe. Nous allons également remplir le champ password_reset_sent_at
, pour savoir à quel moment le token doit expirer, puis sauvegarder l'utilisateur pour stocker ces données. Une fois cela fait, nous allons passer l'utilisateur à un UserMailer
pour envoyer les mails de réinitialisation.
def send_password_reset generate_token(:password_reset_token) self.password_reset_sent_at = Time.zone.now save! UserMailer.password_reset(self).deliver end
Nous n'avons pas encore créé le UserMailer
, nous allons donc le faire.
$ rails g mailer user_mailer password_reset
Dans le mailer, nous allons assigner l'utilisateur à une variable d'instance, de façon à pouvoir accéder à ses données depuis le template, puis spécifier le destinataire et le sujet.
class UserMailer < ActionMailer::Base default from: "from@example.com" def password_reset(user) @user = user mail :to => user.email, :subject => "Password Reset" end end
Dans le template nous allons écrire quelques instructions et fournir un lien vers la réinitialisation de mot de passe.
To reset your password click the URL below.
<%= edit_password_reset_url(@user.password_reset_token) %>
If you did not request your password to be reset please ignore this email and your password will stay as it is.
Le lien dans le mail envoie l'utilisateur vers l'action edit
de PasswordResetsController
. Techniquement, ce n'est pas l'approche la plus RESTful mais cela suffira dans notre cas. Pour que les URLs dans les mailers fonctionnent, nous allons devoir modifier la configuration de notre environnement et ajouter la ligne suivante dans development.rb
.
Auth::Application.configure do # Other config items omitted. config.action_mailer.default_url_options = { :host => "localhost:3000" } end
Nous allons ajouter une ligne similaire dans production.rb
avec le vrai nom de domaine.
Faisons un essai. Si nous visitons la page de réinitialisation de mot de passe et saisissons notre adresse e-mail, nous devrions être informé du fait qu'un mail contenant les informations de réinitialisation a été envoyé.
Lorsque nous consultons le log de développement, nous voyons les détails du mail.
Sent mail to eifion@asciicasts.com (65ms) Date: Thu, 14 Jul 2011 20:18:48 +0100 From: from@example.com To: eifion@asciicasts.com Message-ID: <4e1f4118af661_31a81639e544652a@noonoo.home.mail> Subject: Password Reset Mime-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit To reset your password click the URL below. http://localhost:3000/password_resets/DeStUAsv2QTX_SR3ub_N0g/edit If you did not request your password to be reset please ignore this email and your password will stay as it is. Redirected to http://localhost:3000/ Completed 302 Found in 1889ms
Le mail inclue un lien contenant l'URL de réinitialisation. Cette URL contient le token de réinitialisation en tant que paramètre id
.
Nous devons ensuite écrire l'action edit
. Dedans nous allons récupérer l'utilisateur à partir du token de réinitialisation. Notez que nous utilisons la méthode avec un point d'exclamation de façon à ce que, dans le cas où l'utilisateur n'est pas trouvé, un erreur 404 soit remontée.
def edit @user = User.find_by_password_reset_token!(params[:id]) end
Dans la vue associée, nous allons créer un formulaire permettant à l'utilisateur de réinitialiser son mot de passe.
<h1>Reset Password</h1> <%= form_for @user, :url => password_reset_path(params[:id]) do |f| %> <% if @user.errors.any? %> <div class="error_messages"> <h2>Form is invalid</h2> <ul> <% for message in @user.errors.full_messages %> <li><%= message %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= f.label :password %> <%= f.password_field :password %> </div> <div class="field"> <%= f.label :password_confirmation %> <%= f.password_field :password_confirmation %> </div> <div class="actions"><%= f.submit "Update Password" %></div> <% end %>
Nous utilisons form_for
dans ce formulaire puisque nous modifions une ressource. Pour cette raison, nous devons spécifier explicitement le paramètre :url
de façon à ce que le formulaire n'envoie pas en POST à UsersController
. À la place, il enverra à l'action update
de PasswordResetsController
, en passant le token en tant que paramètre id
. Le formulaire contient une section permettant d'afficher les messages d'erreur et les champs prévus pour saisir et confirmer le nouveau mot de passe.
Nous allons écrire l'action update
. Elle va d'abord vérifier que le token est vieux de moins de deux heures ; dans le cas contraire, nous redirigeons l'utilisateur vers le formulaire de réinitialisation. Si le token est assez récent, nous tentons de mettre à jour l'utilisateur. En cas de succès, nous redirigeons l'utilisateur vers la page d'accueil et affichons un message ; sinon, il doit y avoir une erreur dans le formulaire, nous l'affichons donc de nouveau.
def update @user = User.find_by_password_reset_token!(params[:id]) if @user.password_reset_sent_at < 2.hours.ago redirect_to new_password_reset_path, :alert => "Password ↵ reset has expired." elsif @user.update_attributes(params[:user]) redirect_to root_url, :notice => "Password has been reset." else render :edit end end
Nous pouvons faire un essai en copiant l'URL du mail dans le navigateur.
Si nous saisissons des mots de passe qui ne correspondent pas, nous allons voir un message d'erreur mais, lorsque nous soumettons le formulaire correctement, notre mot de passe est réinitialisé avec succès.
Nous pouvons utiliser cette idée de réinitialisation de mot de passe pour ajouter de nouvelles fonctionnalités, pour confirmer les nouvelles inscriptions. C'est très semblable à la réinitialisation de mot de passe mais au lieu de changer le mot de passe, lorsque le lien est cliqué, nous initialisons un marqueur en base de données pour indiquer que l'inscription est confirmée.
C'est tout pour cet épisode sur la mémorisation de connexion et la réinitialisation de mot de passe. Quelques outils, comme Devise, fournissent ces fonctionnalités mais il peut être utile de le faire de zéro, particulièrement si vous avez besoin de beaucoup de personnalisation.