[RUBY] J'ai changé la façon dont le didacticiel Rails fonctionne: Notes du didacticiel Rails - Chapitre 9

Actuellement auto-apprentissage, le premier tour du tutoriel Rails Si vous voulez défier au deuxième tour Je veux essayer le développement piloté par les tests (TDD) J'ai décidé de lister les exigences à implémenter dans le thème de chaque chapitre

En tant que flux de tâches que j'imagine dans la deuxième semaine Définition des exigences (laissez ici la première semaine)> Ecrivez un test (connectez-vous ici la deuxième semaine)> Implémentation (Si c'est loin de la réalité, veuillez le signaler tôt)

Objectif: pouvoir rester connecté plus longtemps et en toute sécurité

Connaissance de ce chapitre

Sans rapport avec le contenu de ce chapitre Modélisez le résultat pour améliorer la qualité de l'apprentissage Augmentation écrasante du montant (iuput: sortie = environ 2: 8) La vitesse de progression a considérablement diminué, mais j'estime que la qualité de l'apprentissage s'est améliorée.

Difficile d'équilibrer l'efficacité du temps et l'effet d'apprentissage

Introduction de l'éditeur de démarques pour Mac pour améliorer autant que possible l'efficacité du temps

Exigences

Générez un cookie qui sera conservé même si vous fermez le navigateur lors de la connexion

--Enregistrer l'ID utilisateur et le jeton de mémoire dans le cookie

Restez connecté en tant qu'utilisateur sur la base de données qui correspond au cookie lorsque le navigateur est fermé (current_user = nil)

--Si ce n'est pas current_user = nil, le traitement suivant est inutile

Déconnexion complète réussie

Remarque: Chapitre 9 Mécanisme de connexion évolutif

9.1 Fonction Remember me

9.1.1 Jeton de stockage et chiffrement

Risque d'exposition aux cookies et que faire

Extraire les cookies directement des paquets réseau qui transitent par un réseau mal géré avec un logiciel spécial appelé Packet Sniffer > Contre-mesure: SSL (pris en charge)

Extraire le jeton de stockage de la base de données > Solution de contournement: jetons de hachage stockés dans la base de données

Voler l'accès en utilisant directement l'ordinateur ou le smartphone sur lequel l'utilisateur est connecté > Solution de contournement: changer de jeton une fois déconnecté (Tutoriel Rails 6e édition)

Ajout de l'attribut Remember_digest au modèle utilisateur

$ rails generate migration add_remember_digest_to_users remember_digest:string
$ rails db:migrate

Je veux pouvoir gérer l'attribut Remember_token dans le modèle utilisateur Cet attribut n'est pas enregistré dans le DB > Utilisez ʻattr_accessor`

Pourquoi vous avez besoin d'attr_accessor et pourquoi vous n'avez pas (Merci à Rails): Mémorandum du Tutoriel Rails - Chapitre 9

Définir ʻUser.new_tokendans le modèle utilisateur (retourner le jeton de mémoire) SecureRandom.urlsafe_base64` convient pour générer des jetons de stockage

$ rails console
>> SecureRandom.urlsafe_base64
=> "brl_446-8bqHv87AQzUj_Q"
  def User.new_token
    SecureRandom.urlsafe_base64
  end

Hash le jeton de stockage et enregistrez-le dans la base de données Définir «se souvenir» dans le modèle utilisateur Vous pouvez utiliser ʻUser.digest (string) `pour le hachage

  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

«self» est requis

Je veux aussi que le souvenir soit exécuté lorsque je me connecte C'est après ça

9.1.2 Rester connecté

Je souhaite enregistrer l'ID utilisateur dans le navigateur «Signé» avec des cookies signés

cookies.signed[:user_id] = user.id

Décrypté avec cookies.signed [: user_id]

La persistance des cookies est possible avec permanent Dans la chaîne de méthodes

cookies.permanent.signed[:user_id] = user.id

Définir «authentifié?» Dans le modèle utilisateur Le comportement est le même que lors de l'utilisation d'un mot de passe, mais vous pouvez l'imaginer, mais la compréhension partielle de BCrypt ... est insuffisante.

#Renvoie true si le jeton passé correspond au condensé
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

L'argument Remember_token est une variable locale

Changer le comportement lors de la connexion Ajout de Remember (user) Ceci est différent de la méthode Remember du modèle User (prenant un argument)

app/controllers/sessions_controller.rb


  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && 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

Remember (user) est défini comme un assistant Appelez lorsque vous vous connectez Génère des jetons de stockage, les enregistre dans des cookies et enregistre des jetons de résumé dans une base de données (L'importance de l'utilisation de différents helpers et la priorité de la méthode portant le même nom ne sont pas entièrement comprises)

app/helpers/sessions_helper.rb


  #Rendre la session d'un utilisateur persistante
  def remember(user)
    user.remember#Méthode du modèle utilisateur (génération du jeton de stockage, stockage DB du jeton de résumé)
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

current_user n'est pas qu'une session Être maintenu par les cookies

app/helpers/sessions_helper.rb


  #Renvoie l'utilisateur correspondant au cookie de jeton de mémoire
  def current_user
    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 && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

La répétition peut être omise avec (user_id = session [: user_id]) = n'est pas une opération logique, une affectation

À ce stade, «test des rails» est (ROUGE)

FAIL["test_login_with_valid_information_followed_by_logout", #<Minitest::Reporters::Suite:0x0000556848d6b040 @name="UsersLoginTest">, 1.7997455329999923]
 test_login_with_valid_information_followed_by_logout#UsersLoginTest (1.80s)
        Expected at least 1 element matching "a[href="/login"]", found 0..
        Expected 0 to be >= 1.
        test/integration/users_login_test.rb:36:in `block in <class:UsersLoginTest>'

Je ne vois pas le lien pour me connecter En d'autres termes, il semble que vous ne vous soyez pas déconnecté (Devrait maintenir current_user par cookie)

9.1.3 Oubliez l'utilisateur

DB Remember_digest lors de la déconnexion Supprimer le cookie du navigateur (mise à jour avec zéro)

Tout d'abord, définissez forget pour faire fonctionner la base de données

app/models/user.rb


 #Supprimer les informations de connexion de l'utilisateur
  def forget
    update_attribute(:remember_digest, nil)
  end

Ensuite, définissez la méthode d'assistance forget (user) Nil : Remember_digest sur la base de données avec ʻuser.forgetplus tôt Aucun cookie de navigateur aveccookies.delete`

/sample_app/app/helpers/sessions_helper.rb


 def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

Appelez cette méthode d'assistance forget (user) avec la même méthode d'assistance log_out

/sample_app/app/helpers/sessions_helper.rb


 def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end

Maintenant, log_out peut vider la session et le cookie Déconnexion complète possible

rails test est (VERT)

9.1.4 Deux bugs discrets

Résolvez les bogues qui se produisent lors de l'utilisation de plusieurs onglets et navigateurs

Premièrement, si vous ouvrez plusieurs onglets tout en étant connecté en même temps, Problèmes causés par la possibilité de passer plusieurs fois à la déconnexion (delete logout_path)

Puisque current_user = nil est défini dans le premier delete logout_path Si vous demandez à nouveau delete logout_path, le contrôleur appellera à nouveau la méthode log_out. Quand j'essaye d'exécuter forget (current_user), j'obtiens une erreur car current_user = nil

NoMethodError: undefined method `forget' for nil:NilClass
app/helpers/sessions_helper.rb:36:in `forget'
app/helpers/sessions_helper.rb:24:in `log_out'
app/controllers/sessions_controller.rb:20:in `destroy'

Pour pouvoir le détecter dans un test Très simple étant donné que vous devez demander à nouveau delete logout_path

test/integration/users_login_test.rb


require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "login with valid information followed by logout" do
    get login_path
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    assert is_logged_in?
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
    delete logout_path
    assert_not is_logged_in?
    assert_redirected_to root_url
    #↓ Ici encore supprimer la déconnexion_Chemin de la demande
    delete logout_path
    follow_redirect!
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", logout_path,      count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
  end
end

Lorsque vous exécutez le test (ROUGE) Confirmez que le bogue peut être reproduit avec succès

Lorsqu'il n'est pas connecté Pour la requête delete logout_path La méthode log_out ne doit pas être appelée

Vous pouvez utiliser la méthode log_in pour confirmer votre connexion

  def logged_in?
    !current_user.nil?
  end

app/controllers/sessions_controller.rb


  def destroy
    log_out if logged_in?
    redirect_to root_url
  end

Devient log_out si connecté_in? Est-ce une manière à la mode d'écrire Peut être remplacé par ʻif ... end`

[Ruby] Les abus sont strictement interdits! ?? Cas où l'écriture avec un suffixe rend la lecture plus difficile Cette personne a été désignée ainsi du point de vue de la lisibilité et aimerait s'y référer.

Dans cet état, rails test est (VERT)

Suivant, A, B, deux navigateurs sont ouverts, déconnectez-vous avec un navigateur B (DB Remember_digest est nil), Puis fermez le navigateur A (la session [: user_id] du navigateur A est nulle) Problèmes causés par le fait de ne laisser que les cookies stockés dans le navigateur A

Si vous rouvrez le navigateur A dans cet état, le cookie reste.

python


def current_user
  if (user_id = session[:user_id])
    @current_user ||= User.find_by(id: user_id)
  elsif (user_id = cookies.signed[:user_id]) #Devenez vrai ici
    user = User.find_by(id: user_id)
    if user && user.authenticated?(cookies[:remember_token])#Erreur
      log_in user
      @current_user = user
    end
  end
end

ʻSi l'utilisateur && user.authenticated? (Cookies [: Remember_token]) est exécuté Parce que Remember_digest est défini sur nil` en raison du comportement d'un autre navigateur

python


  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)#remember_digest = nil
  end

Renvoie une exception en raison d'une incohérence avec le contenu du cookie

BCrypt::Errors::InvalidHash: invalid hash

Pour pouvoir le détecter dans le test La situation du navigateur A doit être reproduite avec l'objet de modèle utilisateur. En d'autres termes, préparez un modèle avec Remember_digest = nil

test/models/user_test.rb


require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "[email protected]",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "authenticated? should return false for a user with nil digest" do
    assert_not @user.authenticated?('')
  end
end

Le @ user créé par la méthode de configuration correspond exactement à cela, vous pouvez donc utiliser ceci Actuellement Remember_digest = nil @ user.authenticated? ('') Renvoie une exception et le test est (RED)

Si Remember_digest = nil Je veux que @ user.authenticated? ('') Renvoie false

  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

Devrait être Avec Progate, je l'ai souvent utilisé à la fin de l'instruction If, donc je ne le remarque pas. return quitte la méthode et renvoie la valeur Donc rien de moins n'est exécuté

Maintenant, le test est (VERT)

9.2 Case à cocher Se souvenir de moi

Les cases à cocher peuvent être insérées avec la méthode d'assistance

html:/sample_app/app/views/sessions/new.html.erb


      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

Ajouter CSS

app/assets/stylesheets/custom.scss


.
.
.
/* forms */
.
.
.
.checkbox {
  margin-top: -10px;
  margin-bottom: 10px;
  span {
    margin-left: 20px;
    font-weight: normal;
  }
}

#session_remember_me {
  width: auto;
  margin-left: 0;
}

params [: session] [: Remember_me] Reçoit la valeur 1 '' si elle est activée, 0 '' si elle est désactivée "" 1 "ou" "0" au lieu de "1" ou "0"

Ajoutez ce qui suit à l'action create du contrôleur Sessions

if params[:session][:remember_me] == '1'
  remember(user)
else
  forget(user)
end

Ce qui précède peut être réécrit comme suit (opérateur ternaire)

params[:session][:remember_me] == '1' ? remember(user) : forget(user)

Achèvement du mécanisme de connexion permanente jusqu'à présent

9.3 Test [Remember me]

9.3.1 Tester la case Se souvenir de moi

Définir des méthodes d'assistance qui permettent aux utilisateurs de se connecter dans le test

Peut être utilisé pour le test unitaire et le test d'intégration respectivement Définissez la méthode log_in_as dans chacune des classes ActiveSupport :: TestCase et class ActionDispatch :: IntegrationTest

Parce que le test d'intégration ne peut pas gérer la session directement Utilisez post login_path dans la méthode log_in_as pour les tests d'intégration

test/test_helper.rb


ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
  fixtures :all

  #Renvoie true si l'utilisateur du test est connecté
  def is_logged_in?
    !session[:user_id].nil?
  end

  #Connectez-vous en tant qu'utilisateur test
  def log_in_as(user)
    session[:user_id] = user.id
  end
end

class ActionDispatch::IntegrationTest

  #Connectez-vous en tant qu'utilisateur test
  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

rails test(GREEN)

9.3.1 Exercice:

Ayons deux problèmes

En principe, le «@ user» défini par la méthode «setup» dans le test d'intégration N'inclut pas l'attribut Remember_token

Pourquoi vous avez besoin d'attr_accessor et pourquoi vous n'avez pas (Merci à Rails): Mémorandum du Tutoriel Rails - Chapitre 9

/sample_app/app/controllers/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

/sample_app/test/integration/users_login_test.rb


  test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_equal cookies[:remember_token], assigns(:user).remember_token
  end

9.3.2 Test [Se souvenir de moi]

Test du comportement de current_user

ʻAssert_equal , `

/sample_app/test/helpers/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

9.4 Enfin

Si vous procédez comme suit, il sera temporairement inaccessible La page de maintenance peut être affichée

$ heroku maintenance:on
$ git push heroku
$ heroku run rails db:migrate
$ heroku maintenance:off

Recommended Posts

J'ai changé la façon dont le didacticiel Rails fonctionne: Notes du didacticiel Rails - Chapitre 9
11.1 Ressource AccountActivations: Mémorandum du didacticiel Rails - Chapitre 11
Piped together grep?: Mémorandum du Tutoriel Rails - Chapitre 8
J'ai créé une fonction de réponse pour l'extension Rails Tutorial (Partie 1)
[Rails] J'ai essayé de supprimer l'application
J'ai créé une fonction de réponse pour l'extension Rails Tutorial (Partie 5):
Test des messages d'erreur: Notes du didacticiel Rails - Chapitre 7
11.2 Envoyer un e-mail d'activation de compte: Notes du didacticiel Rails - Chapitre 11
rails tutry
tutoriel sur les rails
rails tutry
tutoriel sur les rails
rails tutry
tutoriel sur les rails
tutoriel sur les rails
Où je suis resté coincé dans le "tutoriel sur les rails" d'aujourd'hui (08/10/20)
[Rails] Quand j'utilise form_with, l'écran se fige! ??
[Rails] J'ai essayé de faire passer la version de Rails de 5.0 à 5.2
J'ai essayé d'organiser la session en Rails
Où je suis resté coincé dans le "tutoriel des rails" d'aujourd'hui (05/10/2020)
Code utilisé pour connecter Rails 3 à PostgreSQL 10
Où je suis resté coincé dans le "tutoriel des rails" d'aujourd'hui (06/10/20)
Où je suis resté coincé dans le "tutoriel sur les rails" d'aujourd'hui (04/10/20)
J'ai essayé de configurer tomcat pour exécuter le servlet.
Où je suis resté coincé dans le "tutoriel sur les rails" d'aujourd'hui (07/10/20)