Cet article approfondit ma compréhension en écrivant un article de commentaire du tutoriel Rails pour solidifier davantage mes connaissances Cela fait partie de mon étude. Dans de rares cas, un contenu ridicule ou un contenu incorrect peut être écrit. Notez s'il vous plaît. J'apprécierais que vous me disiez implicitement ...
La source Tutoriel Rails 6e édition
-Ajouter une fonction pour stocker les informations de connexion à la discrétion de l'utilisateur et se connecter même si le navigateur est redémarré.
↑ Comme mentionné ci-dessus, implémentez la fonction pour conserver la connexion même si le navigateur est fermé (Remember me) Créez une branche thématique et commencez.
Comme il est assez difficile de travailler et de créer à partir de maintenant, vérifiez les connaissances à l'avance.
・ Qu'est-ce qu'un jeton? C'est comme un mot de passe utilisé par un ordinateur. Les mots de passe sont créés par des humains et gérés par des humains, mais les jetons sont créés par des ordinateurs et gérés par des ordinateurs.
・ À propos des cookies persistants et des sessions temporaires Pour la session temporaire créée dans le chapitre précédent, la méthode de session a été utilisée pour créer une session dans les cookies dont la date d'expiration correspond à la fermeture du navigateur. Cette fois, nous utiliserons la méthode des cookies pour créer une session avec une date d'expiration infinie (pour être exact, environ 20 ans). Contrairement à la méthode de session, la méthode des cookies ne protège pas les informations et est la cible d'une attaque appelée détournement de session. En enregistrant l'ID utilisateur et le jeton de stockage sous forme d'ensemble dans les cookies et en enregistrant le jeton haché dans la base de données Assurer la sécurité.
・ Quel type de traitement est utilisé pour l'implémenter?
J'ai vérifié le contenu à peu près Ajoutez immédiatement un résumé de la mémoire (Remember_digest) à la base de données.
string
Comme expliqué précédemment, si vous ajoutez une colonne à la table users en ajoutant to_users à la fin du nom de fichier, elle sera reconnue sans autorisation.
Comme Remember_digest n'est pas lisible par l'utilisateur, il n'est pas nécessaire d'ajouter un index.
Par conséquent, il sera migré tel quel.
Quoi utiliser pour créer un jeton de mémoire
Les chaînes longues et aléatoires sont préférées.
La méthode ```urlsafe_base64``` du module SecureRandom correspond à l'objectif, nous allons donc l'utiliser.
Cette méthode utilise 64 types de caractères et renvoie une chaîne de caractères aléatoires de longueur 22.
Le jeton de stockage sera automatiquement généré à l'aide de cette méthode.
```irb
>> SecureRandom.urlsafe_base64
=> "Rr2i4cNWOwhtDeVA4bnT2g"
>> SecureRandom.urlsafe_base64
=> "pQ86_IsKILLv4AxAnx9iHA"
Comme le mot de passe, le jeton peut être dupliqué avec d'autres utilisateurs⁻, mais en utilisant un unique À moins que l'ID utilisateur et le jeton ne soient volés, cela n'entraînera pas de piratage de session.
Définissez une méthode pour créer (générer) un nouveau jeton dans le modèle utilisateur.
def User.new_token
SecureRandom.urlsafe_base64
end
Cette méthode ne nécessite pas non plus d'objet utilisateur, elle est donc définie comme une méthode de classe.
Ensuite, créez la méthode Remember.
Cette méthode enregistre le résumé de stockage correspondant au jeton dans la base de données.
Remember_digest existe dans DB, mais Remember_token n'existe pas.
Je souhaite uniquement enregistrer le résumé dans la base de données, mais je veux enregistrer le résumé du jeton associé à l'objet utilisateur.
Je souhaite également accéder à l'attribut de jeton.
En d'autres termes, un jeton est requis comme attribut virtuel comme dans le cas d'un mot de passe.
Has_secure_password a été généré automatiquement lorsque le mot de passe a été implémenté, mais cette fois
attr_accessor
N'oubliez pas d'utiliser_Créez un jeton.
user.rb
class User < ApplicationRecord
attr_accessor :remember_token
# before_save { self.email.downcase! }
# has_secure_password
# VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
# validates :name, presence: true, length:{maximum: 50}
# validates :email, presence: true, length:{maximum: 255},
# format: {with: VALID_EMAIL_REGEX},uniqueness: true
# validates :password, presence: true, length:{minimum: 6}
# def User.digest(string)
# cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
# BCrypt::Engine::cost
# BCrypt::Password.create(string, cost: cost)
# end
# def User.new_token
# SecureRandom.urlsafe_base64
# end
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest,User.digest(remember_token))
end
end
Première ligne de la méthode Remember
self.remember_token = User.new_le jeton est
Obligatoire ici car si vous n'écrivez pas self, une variable locale appelée Remember_token sera créée.
Puisque le mot de passe est inaccessible ici, update_attribute est utilisé pour contourner la validation.
##### Exercice
1. Bougez fermement.
Remember_token est une chaîne de 22 caractères générée aléatoirement
Vous pouvez voir que Remember_digest est leur chaîne hachée.
```irb
>> user.remember
(0.1ms) begin transaction
User Update (2.4ms) UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ? [["updated_at", "2020-06-17 14:30:27.202627"], ["remember_digest", "$2a$12$u8cnBDCQX85e9gBHbQWWNeJq.mxKq8hhpt/FSYMtm2rI2ljWIhRLi"], ["id", 1]]
(6.1ms) commit transaction
=> true
>> user.remember_token
=> "lZaXgeF42y5XeP-EEPzstw"
>> user.remember_digest
=> "$2a$12$u8cnBDCQX85e9gBHbQWWNeJq.mxKq8hhpt/FSYMtm2rI2ljWIhRLi"
>>
class << self
Si vous utilisez, tout jusqu'à la fin est défini comme une méthode de classe.
Notez que le mot-clé self représente ici la classe User elle-même, pas l'objet d'instance.Utilisez la méthode `` cookies '' pour enregistrer dans des cookies persistants. Il peut être utilisé comme une session de type hachage.
les cookies ont une valeur et expirent
cookies[:remember_token] = { value: remember_token, expires: 20.years.from_now.utc }
Ce faisant, la valeur de Remember_token avec une date d'expiration de 20 ans peut être enregistrée dans les cookies [: Remember_token]. De plus, comme la date d'expiration de 20 ans est souvent utilisée, une méthode dédiée a été ajoutée à Rails.
cookies.permanent[:remember_token] = remember_token
Cela a le même effet.
De plus, l'ID utilisateur est enregistré dans les cookies persistants, mais si vous l'enregistrez tel quel, l'ID sera enregistré tel quel, Parce que l'attaquant sera confus quant au format dans lequel les cookies sont stockés. Crypter. Utilisez des cookies signés pour le cryptage.
cookies.signed[:user_id] = user.id
Vous pouvez maintenant le crypter et l'enregistrer en toute sécurité.
Bien sûr, l'ID utilisateur doit également être enregistré en tant que cookies persistants, utilisez-le donc en connectant la méthode permanente.
#### **`cookies.permanent.signed[:user_id] = user.id`**
En plaçant l'ID utilisateur et le jeton de mémoire dans les cookies comme un ensemble comme celui-ci Lorsqu'un utilisateur se déconnecte, il ne peut pas se connecter (car le condensé de base de données est supprimé)
Enfin, comment comparer le jeton stocké dans le navigateur avec le condensé de la base de données Une partie du code source de secure_password
BCrypt::Password.new(remember_digest) == remember_token
Utilisez un code comme celui-ci.
Ce code compare directement Remember_digest et Remember_token.
En fait, Bcrypt a redéfini l'opérateur ==, et ce code
#### **`BCrypt::Password.new(remember_digest).is_password?(remember_token)`**
Il fonctionne. Utilisez ceci pour définir une méthode ```authenticated? '' `` Qui compare un résumé de stockage avec un jeton de stockage.
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
Remember_digest ici est identique à self.remember_digest. Compare le résumé de stockage de la base de données avec le jeton de stockage passé à l'argument et renvoie true s'il est correct
Ajoutez immédiatement le traitement de rappel à la partie de traitement de connexion de sessions_controller.
def create
user = User.find_by(email: params[:session][:email].downcase)
if user&.authenticate(params[:session][:password])
log_in(user)
remember user
redirect_to user
else
flash.now[:danger] = "Invalid email/password combination"
render 'new'
end
end
Ici, nous utilisons la méthode d'aide à la mémoire. (Pas encore défini)
↓ rappelez-vous de la méthode d'aide
sessions_helper.rb
def remember(user)
user.remember
cookies.signed.permanent[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
Supplément car il est difficile à comprendre. Avec la méthode Remember définie dans le modèle User
user.rb
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest,User.digest(remember_token))
end
Générez un jeton de mémoire et un résumé de mémoire pour un objet utilisateur.
Avec la méthode Remember définie dans sessions_helper
Flux de. Notez que le nom de la méthode est couvert.
Vous pouvez désormais stocker en toute sécurité vos informations d'utilisateur dans des cookies, mais regardez votre statut de connexion La méthode `` utilisateur_actuel '' utilisée pour modifier dynamiquement la mise en page est uniquement pour les sessions temporaires Ce n'est pas pris en charge, alors corrigez-le.
def current_user #Renvoie l'objet utilisateur actuellement connecté
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user &. authenticated?(cookies[remember_token])
log_in user
@current_user = user
end
end
end
-La duplication de code est réduite en utilisant une variable locale appelée user_id. ・ Lorsque le navigateur est ouvert pour la première fois, les cookies persistants sont traités et le traitement de connexion est également effectué en même temps. L'utilisateur est stocké dans @current_user jusqu'à ce que le navigateur soit fermé.
Pour le moment, il n'y a aucun moyen de supprimer le processus de déconnexion (cookies persistants) Je ne peux pas me déconnecter. (L'action de déconnexion existante supprime uniquement la session temporaire, récupérez donc les informations à partir des cookies persistants Je ne peux pas me déconnecter car je me connecte automatiquement. )
Oui.
Cela fonctionne.
>> user = User.first
(1.1ms) SELECT sqlite_version(*)
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "take", email: "[email protected]", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-18 15:18:53", password_digest: [FILTERED], remember_digest: "$2a$12$tAZFCVr39lkPONLS4/7zneYgOE5pcYDM2kX6F1yKew2...">
>> user.remember
(0.1ms) begin transaction
User Update (2.8ms) UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ? [["updated_at", "2020-06-18 15:23:21.357804"], ["remember_digest", "$2a$12$h3K3aZSBmXB7wGkNdsBrS.2/UaawMQ199DGMvTDU8upvvOKCzbeba"], ["id", 1]]
(10.3ms) commit transaction
=> true
>> user.authenticated?(user.remember_token)
=> true
Je ne peux pas me déconnecter car je n'ai pas supprimé les cookies persistants.
Définissez la méthode oublier '' pour résoudre ce problème. Définissez le résumé de la mémoire sur nul avec cette méthode. De plus, en définissant la méthode
forget``` dans sessions_helper également
Cela supprime également l'ID utilisateur et le jeton de stockage stockés dans les cookies.
def forget(user) #Supprimer la session persistante / réinitialiser le résumé de la mémoire
user.forget
cookies.delete[:user_id]
cookies.delete[:remember_token]
end
def log_out
forget(current_user)
session.delete(:user_id)
@current_user = nil
end
Passons en revue le flux de traitement de déconnexion.
Pour le moment, il reste deux bugs. C'est assez gênant, je vais donc l'expliquer en détail.
Premier bug Lorsque vous êtes connecté sur plusieurs onglets, déconnectez-vous sur l'onglet 1, puis déconnectez-vous sur l'onglet 2. Après vous être déconnecté à l'aide de la méthode log_out dans l'onglet 1, current_user est nul. Si vous essayez de vous déconnecter à nouveau dans cet état, cela échouera car le cookie à supprimer est introuvable.
Deuxième bug Lorsque vous êtes connecté avec un autre navigateur (Chrome, Firefox, etc.).
Pour corriger ce bogue, écrivez d'abord un test pour attraper le bogue Écrivez du code pour y remédier.
delete logout_path
Reproduisez la déconnexion deux fois en l'insérant à nouveau après le processus de déconnexion du test de connexion.
Pour réussir ce test Vous ne devez vous déconnecter que lorsque vous êtes connecté.
def destroy
log_out if logged_in?
redirect_to root_url
end
Concernant le second bug, il est difficile de reproduire différents environnements de navigateur dans le test, donc Ne testez que Remember_digest dans le modèle User. Plus précisément, il teste qu'il renvoie false lorsque Remember_digest est nul.
test "authenticated? should return false for a user with nil digest" do
assert_not @user.authenticated?('')
end
Améliorer authentifié? Méthode pour réussir le test
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
Si Remember_digest vaut nil, renvoie immédiatement false avec le mot-clé return et met fin au processus.
Cela corrige deux bogues.
Ensuite, implémentez une case à cocher indispensable pour la fonction Remember me (une fonction qui ne se souvient que lorsqu'elle est cochée)
<%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %>
<span>Remember me on this computer</span>
<% end %>
Pour la raison de son placement à l'intérieur de l'étiquette, voir https://html-coding.co.jp/annex/dictionary/html/label/ Ce site est facile à comprendre En d'autres termes, cliquer n'importe où sur l'étiquette peut se comporter comme si vous aviez coché la case.
Une fois que vous l'avez façonné avec CSS, vous êtes prêt à partir. Puisque 1 ou 0 est maintenant entré dans les paramètres [: session] [: Remember_me] dans la case à cocher Vous devez vous en souvenir quand il est 1.
Lorsqu'il est implémenté à l'aide de l'opérateur ternaire
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
remember user
Remplacez simplement la ligne par ceci
À propos, l'opérateur ternaire est
Instruction conditionnelle?Traitement lorsque vrai:Traitement lorsque faux
Peut être écrit dans le format. À propos, puisque toutes les valeurs numériques des paramètres sont enregistrées sous forme de chaînes de caractères, 1 de l'instruction conditionnelle doit être placé entre «». Notez que les fausses minutes seront toujours exécutées et vous ne pourrez pas vous en souvenir.
↑ Mais j'ai écrit une note, mais cela ne fonctionne que si la condition des paramètres est «1». Espérons que les valeurs sont stockées dans des cookies Ça marche bien.
>> hungry = true
=> true
>> hungry ? puts("I'm hungry now") : puts("I'm not hungry now")
I'm hungry now
=> nil
Maintenant que nous avons implémenté Rememberme, nous allons également créer des tests.
params [: session] [: Remember_me] == '1'? Remember (user): forget (user) `` implémenté avec l'opérateur ternaire précédent
Cette partie est 1 (vrai) 0 (faux) pour ceux qui touchent le programme
params[:session][:remember_me] ? remember(user) : forget(user)
Cependant, la case à cocher renvoie 1 et 0.
Dans Ruby, 1 et 0 ne sont pas des valeurs booléennes et les deux sont traités comme vrais, donc ce serait une erreur d'écrire de cette façon.
Vous devez écrire un test qui peut détecter de telles erreurs.
Vous devez vous connecter pour vous souvenir de l'utilisateur. Jusqu'à présent, j'avais l'habitude d'envoyer des hachages de paramètres en utilisant la méthode post un par un.
Comme il est difficile de le faire à chaque fois, définissez une méthode de connexion.
Définissez-le comme une méthode log_in_as pour éviter toute confusion avec la méthode log_in.
#### **`test_helper`**
```rb
class ActiveSupport::TestCase
# Run tests in parallel with specified workers
parallelize(workers: :number_of_processors)
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
include ApplicationHelper
# Add more helper methods to be used by all tests here...
def is_logged_in?
!session[:user_id].nil?
end
def log_in_as(user)
session[:user_id] = user.id
end
end
class ActionDispatch::IntegrationTest
def log_in_as(user, password: 'password', remember_me: '1')
post login_path, params:{ session: { email: user.email,
password: password,
remember_me: remember_me}}
end
end
La méthode log_in_as est définie deux fois séparément dans ActionDispatch :: IntegrationTest et ActiveSupport :: TestCase. Vous ne pouvez pas utiliser la méthode `` session '' dans le test d'intégration. Par conséquent, dans le test d'intégration, je me connecte à la place en utilisant la demande de publication.
En donnant aux deux tests le même nom, vous pouvez utiliser la méthode log_in_as sans vous soucier de quoi que ce soit lorsque vous souhaitez vous connecter à la fois pour l'intégration et les tests unitaires. Il suffit d'appeler.
Maintenant que nous avons défini la méthode log_in_as, nous allons implémenter le test Remember_me.
test "login with remembering" do
log_in_as(@user, remember_me: '1')
assert_not_empty cookies[:remember_token]
end
test "login without remembering" do
log_in_as(@user, remember_me: '1')
delete logout_path
log_in_as(@user, remember_me: '0')
assert_empty cookies[:remember_token]
end
'1')
Puisque la valeur par défaut est définie, ce n'est pas nécessaire, mais l'attribut Remember_me est également entré pour faciliter la comparaison.
##### Exercice
1. Dans le test d'intégration de ↑, j'ai seulement testé que les cookies ne sont pas vides car l'attribut virtuel Remember_token n'est pas accessible.
```assigns```Vous pouvez obtenir la variable d'instance de la dernière action accédée à l'aide de la méthode.
Dans l'exemple du test ci-dessus, car l'action de création de sessions_controller est accessible dans la méthode `` `` log_in_as```.
La valeur de la variable d'instance définie par l'action de création peut être lue à l'aide d'un symbole.
En particulier
Actuellement, l'action de création utilise une variable locale appelée user, alors ajoutez @ à ceci et appelez-le @user.
En le changeant en variable d'instance, la méthode assigns peut être lue.
Après cela, @user peut être lu en définissant ```assigns (: user) `` `dans le test.
#### **`users_login_test.rb`**
```rb
test "login with remembering" do
log_in_as(@user, remember_me: '1')
assert_equal cookies[:remember_token] , assigns(:user).remember_token
end
sessions_controller.rb
def create
@user = User.find_by(email: params[:session][:email].downcase)
if @user&.authenticate(params[:session][:password])
log_in(@user)
params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
redirect_to @user
else
flash.now[:danger] = "Invalid email/password combination"
render 'new'
end
end
J'ai implémenté le traitement des connexions et les méthodes d'assistance liées à la session dans sessions_helper
current_user
Aucun test n'a été effectué sur le processus de branchement de la méthode.
Le remplacement d'une chaîne appropriée qui n'a rien à voir avec la preuve réussira le test.
Test VERT ↓
sessions_helper.rb
def current_user #Renvoie l'objet utilisateur actuellement connecté
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
Le japonais est également autorisé car je ne l'ai pas testé.
user = User.find_by(id: user_id)
if user &.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
end
C'est mauvais, alors créez un fichier de test comme
sessions_helper```.
sessions_helper_test.rb
require 'test_helper'
class SessionsHelperTest < ActionView::TestCase
def setup
@user = users(:michael)
remember(@user)
end
test "current_user returns right user when session is nil" do
assert_equal @user, current_user
assert is_logged_in?
end
test "current_user returns nil when remember digest is wrong" do
@user.update_attribute(:remember_digest, User.digest(User.new_token))
assert_nil current_user
end
end
Le premier test s'assure que l'utilisateur mémorisé et l'utilisateur courant sont les mêmes et qu'ils sont connectés. Ce faisant, le test peut confirmer que le traitement du contenu fonctionne lorsque l'ID utilisateur existe dans les cookies.
Dans le second test, en réécrivant Remember_digest, cela ne correspond pas à Remember_token enregistré par la méthode Remember ''. Le current_user renvoie nil comme prévu, c'est-à-dire la méthode ```authenticated?
`
Je teste que cela fonctionne correctement.
De plus, en complément, la méthode assert_equal
fonctionne même si les premier et deuxième arguments sont échangés.
Notez que vous devez écrire la valeur attendue dans le premier argument et la valeur réelle dans le deuxième argument.
Si vous n'écrivez pas de cette manière, l'affichage du journal ne s'activera pas en cas d'erreur.
Et, bien sûr, le test ne passe pas à ce stade.
Le test réussit en supprimant les déclarations totalement non pertinentes que vous avez incluses. Maintenant que vous pouvez tester n'importe quelle branche de current_user, vous pouvez attraper les bogues de régression.
FAIL["test_current_user_returns_nil_when_remember_digest_is_wrong", #<Minitest::Reporters::Suite:0x000055b13fd67928 @name="SessionsHelperTest">, 1.4066297989993473]
test_current_user_returns_nil_when_remember_digest_is_wrong#SessionsHelperTest (1.41s)
Expected #<User id: 762146111, name: "Michael Example", email: "[email protected]", created_at: "2020-06-20 15:38:57", updated_at: "2020-06-20 15:38:58", password_digest: [FILTERED], remember_digest: "$2a$04$uoeG1eJEySynSb.wI.vyOewe9s9TJsSoI9vtXNYJxrv..."> to be nil.
test/helpers/sessions_helper_test.rb:15:in `block in <class:SessionsHelperTest>'
↑ Alors que la valeur de retour de current_user devrait être nulle, l'objet utilisateur est retourné. C'est une erreur.
Recommended Posts