0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Ruby on Rails チュートリアル第8章をやってみて

Posted at

#基本的なログイン機構
■第8章
基本的なログイン機構を構築していく。

##8.1 セッション
HTTPは状態のないプロトコル。HTTPのリクエスト1つ1つは、それより前のリクエストの情報をまったく利用できない、独立したトランザクションとして扱われる。

ログインするとcreateでセッションを作成して保存。
ログアウトするとdestroyでセッションを破棄。

###8.1.1 Sessionsコントローラ
ログインとログアウトの要素を、Sessionsコントローラの特定のRESTアクションにそれぞれ対応付ける。

セッションコントローラの作成。

$ rails generate controller Sessions new

リソースを追加して標準的なRESTfulアクションをgetできるようにする。
以下のコードを追記。

routes.rb
get    '/login',   to: 'sessions#new'
post   '/login',   to: 'sessions#create'
delete '/logout',  to: 'sessions#destroy'

Sessionsコントローラのテストで名前付きルートを使うようにする。

sessions_controller_test.rb
require 'test_helper'

class SessionsControllerTest < ActionDispatch::IntegrationTest

  test "should get new" do
    get login_path
    assert_response :success
  end
end

###8.1.2 ログインフォーム
新しいセッションフォームを作成するときには、form_forヘルパーに追加の情報を独自に渡さなければならない。
ログインフォームのコード

new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

###8.1.3ユーザーの検索と認証
ログインでセッションを作成する場合、入力が無効な場合の処理を最初に行う。

最小限のcreateアクションをSessionsコントローラで定義し、空のnewアクションとdestroyアクションも作成する。

sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    render 'new'
  end

  def destroy
  end
end

ユーザー認証に必要なあらゆる情報をparamsハッシュから簡単に取り出せる。

Active Recordが提供する
User.find_byメソッドでデータベースからユーザーを探し、has_secure_passwordが提供する
authenticateメソッドでパスワードをチェックする。authenticateメソッドは認証に失敗したときにfalseを返す。

最終的にはこんな感じに。

sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      # エラーメッセージを作成する
      render 'new'
    end
  end

  def destroy
  end
end

###8.1.4 フラッシュメッセージを表示する
セッションではActive Recordのモデルを使っていないので、ログインに失敗したときには代わりにフラッシュメッセージを表示する。

ここで書いたコードは誤りで、フラッシュメッセージが消えない状態になるので、一旦次へ。

###8.1.5 フラッシュのテスト
統合テストを作成する。

$ rails generate integration_test users_login

以下の流れでテストコードを実装していく

・ログイン用のパスを開く

・新しいセッションのフォームが正しく表示されたことを確認する

・わざと無効なparamsハッシュを使ってセッション用パスにPOSTする

・新しいセッションのフォームが再度表示され、フラッシュメッセージが追加されることを確認する

・別のページ(Homeページなど)にいったん移動する

・移動先のページでフラッシュメッセージが表示されていないことを確認する

フラッシュメッセージの残留をキャッチするテスト。

users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  test "login with invalid information" do
    get login_path
    assert_template 'sessions/new'
    post login_path, params: { session: { email: "", password: "" } }
    assert_template 'sessions/new'
    assert_not flash.empty?
    get root_path
    assert flash.empty?
  end
end

このままだとテスト失敗になるので、テストをパスさせるに、本編のコードでflashflash.nowに置き換える。後者はその後リクエストが発生したときに消滅する。

##8.2 ログイン
cookiesを使った一時セッションでユーザーをログインできるようにする。このcookiesは、ブラウザを閉じると自動的に有効期限が切れるものを使う。

ほんとはセッションの実装はめちゃくちゃ大変だけど、Rubyのモジュール機能使えば楽にできる。

application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper
end

###8.2.1 log_inメソッド
同じログイン手法を様々な場所で使いまわせるようにするために、
sessionsヘルパーにlog_inという名前のメソッドを定義。

ログインメソッド

sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
end

上から3行目のコードで、ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成される。

sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
  end
end

createアクションを完了し、ユーザーのプロフィールページにリダイレクトする準備ができた。

###8.2.2 現在のユーザー
ユーザーIDを一時セッションの中に安全に置けるようになったので、今度は、ユーザーIDを別のページで取り出すことにする。

current_userメソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにする。

ユーザーIDが存在しない状態でfindを使うと例外が発生するので、find_byメソッドを使う。

sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end

  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end
end

current_userメソッドが動作するようになったので、ユーザーがログインしているかどうかに応じてアプリケーションの動作を変更するための準備ができた。

###8.2.3 レイアウトリンクを変更する
ユーザーがログイン中の状態とは「sessionにユーザーidが存在している」こと、つまりcurrent_usernilではないという状態を指すので、否定演算子!を使ってチェックをしていく。

sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end

  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end

  # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?
  end
end

###8.2.4 レイアウトの変更をテストする
以下の手順でテストを作成する。
・ログイン用のパスを書く

・セッション用パスに有効な情報をpostする

・ログイン用リンクが表示されなくなったことを確認する

・ログアウト用リンクが表示されていることを確認する

・プロフィール用リンクが表示されていることを確認する

上の変更の確認のため、テスト時に登録済みユーザーとしてログインしておく必要がある。

テスト用データをfixture (フィクスチャ) で作成できる。現時点のテストでは、ユーザーは1人いれば十分。

fixture向けのdigestメソッドを追加する。

以下のコードを追加。

user.rb
# 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end
end

digestメソッドができたので、有効なユーザーを表すfixtureを作成できるようになった。

ユーザーのログインで使うfixture。

users.yml
michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

このコードでテストユーザー用の有効なパスワードが作成できる。

<%= User.digest('password') %>

fixtureは生のパスワードを参照できないので、テスト用のfixtureでは全員同じパスワード「password」にする。

assert_redirected_to @user

上のコードはリダイレクト先が正しいかどうかをチェックしている。

テストでも問題ないことが確認できた。

###8.2.5 ユーザー登録時にログイン
ユーザー登録中にログインを済ませておくことにする。Usersコントローラのcreateアクションにlog_inを追加するだけで済むらしい。

##8.3 ログアウト
ログアウト機能を追加していく。ログアウト用リンクは作成済なので、ユーザーセッションを破棄するための有効なアクションをコントローラで作成する済む。

log_outメソッド。以下を追記。

sessions_helper.rb
# 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end

2行目のコードで、現在のユーザーをnilに設定できる。

ここで定義したlog_outメソッドは、Sessionsコントローラのdestroyアクションでも同様に使っていく。

以下を追記。

sessions_controller.rb
# 現在のユーザーをログアウトする
def destroy
   log_out
   redirect_to root_url
end

テストの内容も弄って、無事テストGREENに。

##感想
ログイン・ログアウト機能の実装はバックエンドよりの作業なのかな?
HTTPだったりcookiesの話は応用情報の時にもちょっと触ったので懐かしいですね。
フワフワしたまま進んでいっていますが、頑張ります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?