Rails Tutorial 8章 ログイン処理(session)

セッション

HTTPはステートレスなプロトコルであり、リクエスト一つ一つが独立したトランザクションとなっている。
つまり、ユーザーIDとかはHTTPプロトコル内には保存できない。

その代わりに状態を保存しているのがセッション。

セッションを作成する
セッションをRESTfulなリソースとしてモデリングしたい。そのほうが便利。new→create→destroyみたいな。

まず、セッションコントローラを作る。

$ rails g controller Sessions new

リソースベースルーティングは無駄が多いため、今回は個別に指定(名前付きルーティングのみ)

.
.
get '/login' => 'sessions#new'
post '/login' => 'sessions#create'
delete '/logout' => 'sessions#destroy'


ちなみに、 rails routesコマンドで今使用しているルーティングを確認できる

$ rails routes
Prefix Verb   URI Pattern               Controller#Action
    root GET    /                         static_pages#home
    help GET    /help(.:format)           static_pages#help
.
.


ログインフォームの作成


新規作成ではActiveRecordがエラーメッセージを持っていたけど、今回のセッションはActiveRecordではないた、エラーメッセージを自分で作成する必要がある。
(セッションはDBであれこれするものではないため、ActiveRecordに関連づけられたエラーメッセージは使えない)


セッションフォームとユーザー登録ページの最大の違いは、セッションはモデルを持たないこと。
つまり、セッションは、@userのようなインスタンス変数を持たない。
 →新しいセッションフォームを作成するときには、form_forヘルパーに追加の独自の情報を渡さなければいけない。

 form_for(@user)

Railsは通常、この記述だけで「フォームのactionは/usersというURLへのPOSTである」と判定するが、
セッションの場合は、リソースの名前と対応するURLを具体的に指定しなければならない。

 form_for(:session, url: login_path)

これを用いて作成したログインフォームがこれ

app/views/sessions/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>

そして、生成されたHTMLフォーム

<form accept-charset="UTF-8" action="/login" method="post">
  <input name="utf8" type="hidden" value="&#x2713;" />
  <input name="authenticity_token" type="hidden"
         value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
  <label for="session_email">Email</label>
  <input class="form-control" id="session_email"
         name="session[email]" type="text" />
  <label for="session_password">Password</label>
  <input id="session_password" name="session[password]"
         type="password" />
  <input class="btn btn-primary" name="commit" type="submit"
       value="Log in" />
</form>

これを詳しく見るに、paramsハッシュに入る値は
params[:session][:email]
params[:session][:password]
になる。




ユーザーの検索と検証



ログイン処理

ログイン失敗

ログイン成功

の順で作成






まず、最小限のcreateアクションを作成

app/controllers/sessions_controller.rb
.
 def new
 end

 def create
  render 'new'
 end

 def destroy
 end


ここで、ログインフォームのデバック情報

---
session:
  email: 'user@example.com'
  password: 'foobar'
commit: Log in
action: create
controller: sessions


paramsを見ると

{ session: { password: "foobar", email: "user@example.com" } }

つまり、

params[:session]

に、

{ password: "foobar", email: "user@example.com" }

が含まれていて、
結果として、

params[:session][:email]
params[:session][:password]

これにアクセスすることになる。

ネストした(入れ子になった)ハッシュになっている。




つまり、createアクションの中では、ユーザー認証に必要な情報をparamsから簡単に取り出せる。

そこで、

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


以下、解説
・if user && ~~~~

&&(論理積)は取得したユーザーが有効かどうかを決定するためのもの。
つまり、
左辺:ユーザーがDBに存在する
右辺:DBのユーザーとparams値の認証に成功する
この場合のみ、if が true になる。



フラッシュメッセージを表示する


セッションではActiveRecordを使ってないので、ログインに失敗した時は、エラーメッセージの代わりにフラッシュメッセージを使用する。

app/controllers/sessions_controller.rb

def create
.
.
 else
  flash[:danger] = 'Invalid email/password combination'
.
.

※フラッシュメッセージはWebサイトのレイアウトに表示されるので、flash[:danger]で設定したメッセージは自動的に表示される。(スタイルもBootStrapで整えられている)



しかし、これはrenderで強制的に再レンダリングされているため、メッセージが消えない。


んで、こう修正

app/controllers/sessions_controller.rb

def create
.
.
 else
  flash.now[:danger] = 'Invalid email/password combination'
.
.

flash.nowとすることで、その後リクエストが発生するとメッセージが消滅する。





ログイン


有効な値の送信をフォームで扱う。
そのため、cookieを使った一時セッションでユーザーをログインできるようにする。(ブラウザを閉じると消滅する)

セッションを一から実装するには、大量のコードを書かなければいけない(らしい)
それを一箇所にパッケージ化するのがモジュール。

セッションコントローラを生成した時点で、セッション用ヘルパーモジュールも自動生成されている。しかも、ビューに自動的に読み込まれる。(Applicationコントローラにモジュールを読み込ませれば、どのコントローラにも使える)



Applicationコントローラにモジュールを読み込ませる

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
 .
 include SessionsHelper
end

設定が完了したので、ユーザーログインのコードへ。




log_inメソッド


railsで事前定義済みのsessionメソッドを使って、単純なログインを行えるようにする。(さっき作ったsessionコントローラとは無関係)

 session[:user_id] = user,id

この処理を実行すると、ブラウザ内の一時cookieに暗号化済みのユーザーIDが自動で作成される。(ブラウザを閉じると破棄される)
 →この後のページで、session[:user_id]を使ってユーザーIDをもとどおりに取り出すことができる。


これを、様々な場所で使えるように、ヘルパーにメソッドを定義

app/helpers/sessions_helper.rb

module SessionsHelper

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

sesisonメソッドで作成された一時cookieは自動的に暗号化され、このコードは保護される。
!攻撃者がこの情報をcookieから盗み出しても、それを使って本物のユーザーとしてはログインできない!
 →しかし、永続cookieは別


このlog_inメソッドを使い、createアクションに追記。

app/controllers/sessions_controller.rb

def create
 user ~~~~
 if ~~~~~
  log_in user
  redirect_to user
.
.

以下のコード

 redirect_to user

を、Railsは自動的に変換して、次のように、プロフィールページへのルーティングにする

 user_url(user)