Rails Tutorial 9章 remember_meチェックボックス

よくあるやつ。「次回以降自動でログインする」みたいな。


ログインフォームに追記

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 %>
.

bootstrapが、checkbox, inlineの二つを認識して、テキストRemember ~~ と同じ行に配置してくれる

もうちょっとスタイルを整える

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ハッシュには、すでにチェックボックスの値が含まれている。(チェックボックスがオンの時は「1」になり、オフの時は「0」になる。
なので、このparamsハッシュの値を調べて判定すれば良い。

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


この式、「三項演算子」というのを使えば一行で書けるっぽい。

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

非常にコンパクト


これを、createアクションに追記

app/controllers/sessions_controller.rb
.
def create
.
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
.


三項演算子
「論理値? ? 何かをする : 別のことをする
if boolean?
var = foo
else
var = bar
end

var = boolean? ? foo : bar

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

現在のユーザー

ユーザーIDをセッションの中に安全におけるようになったので、これを別ページで取り出せるようにしたい。

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

例えば、こんなことをしたい

 <%= current_user.name %>
 redirect_to current_user


現在のユーザーを検索する方法としては、 findメソッドが挙げられる。
しかし、ユーザーIDが存在しない状態でfindメソッドを使うと例外が発生してしまう。(ActiveRecord::RecordNotFoundが出ちゃう)

そこでfind_byメソッドを使いたい。

 User.find_by(id: session[:user_id])

find_byメソッドによって、IDが無効な場合(=ユーザーが存在しない場合)には、メソッドは例外を発生せず nil を返す。


この手法を使ってcurrent_userを定義

 def current_user
  if session[:user_id]
   User.find_by(id:session[:user_id])
  end
 end

nilが返されれば、DBへの問い合わせ回数を抑えられ、負担も減る。


また、Rubyの慣習に従い、実行結果をインスタンス変数に保存している

if @current_user.nil?
 @current_user = User.find_by(id: session[:user_id:)
else
 @current_user
end

or演算子「||」を使えばこんな簡単に

@current_user = @current_user || User.find_by(id: session[:user_id])


これ、論理値自体は常にTrueになる。
よって、@current_userに何も代入されてない時だけfind_byメソッドが呼び出され、無駄な読み出しが防げる。

これを短縮して、

 @current_user ||= User.find_by(id: session[:user:id])


「||=」とは何か?
例)

>>@foo
=>nil
>>@foo = @foo || "bar"
=>"bar"
>>@foo = @foo || "baz"
=>"bar"



以上のコードを適用した結果がこちら

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


レイアウトリンクを変更する

ユーザーがログインしている時とそうでないときでレイアウトを変更したい。

まず、ログインしているかどうかを判別するメソッドを作成(ERBコードの中で条件分岐を使う)

<% if logged_in? %>
 #ログインユーザー用のリンク
<% else %>
 #ログインしていないユーザー用のリンク
<% end %>

これを機能させるため、logged_in?を作る


ユーザーがログイン中の状態とは、「sessionにユーザーIDが存在している」こと。
つまり、current_userがnilではないということ。
つまり、否定演算子「!」を使用して判定。

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

これでユーザーのログイン時にレイアウトを変えられる準備が整った。


んで、書いたビューがこれ

app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        <% if logged_in? %>
          <li><%= link_to "Users", '#' %></li>
          <li class="dropdown">
            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
              Account <b class="caret"></b>
            </a>
            <ul class="dropdown-menu">
              <li><%= link_to "Profile", current_user %></li>
              <li><%= link_to "Settings", '#' %></li>
              <li class="divider"></li>
              <li>
                <%= link_to "Log out", logout_path, method: :delete %>
              </li>
            </ul>
          </li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

レイアウトに新しいリンクを追加したので、Bootstrapのドロップダウンメニュー機能を適用できる状態になった。(dropdown-menu, dropdownとか)

これらの機能を有効にするため、Railsのapplication.jsファイルに指示出す。

app/assets/javascripts/application.js
.
//= require jquery
//= require bootstrap
.

ユーザー登録時にログイン

log_in @user これだけ

app/controllers/users_controller.rb
.
.
def create
.
if @user.save
 log_in @user
.
.

ログアウト

セッションからユーザーIDを削除ればいい。

session.delete(:user_id)

これで、現在のユーザーをnilにできる。

ログイン処理と同じようにsessionヘルパーに記載

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


ここで定義したlog_outメソッドをsesisonコントローラのdestroyアクションでも使う。

app/controllers/sessions_controller.rb
.
.
def destroy
 log_out
 redirect_to root_url
end

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)

Rails Tutorial 7章 編集中 ユーザー登録フォーム(エラーメッセージ)

エラーメッセージ


errors.full_messagesオブジェクトはエラーメッセージの配列を持ってる。

例えば

$ rails console
.
>>user.save
=>false
>>user.error.full_message
=> ["Email is invalid", "Password is too short (minimum is 6 characters)"]

ここでは、保存に失敗した際、newページに戻って、エラーメッセージのパーシャル出力して、エラーメッセージを表示する。

app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= f.label :name %>
      <%= f.text_field :name, class: 'form-control' %>

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

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

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

      <%= f.submit "Create my account", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>


パーシャルは、'shared/error_messages'てやつ
複数のビューで使用されるパーシャルは、sharedってディレクトリによく置かれるらしい。

sharedを作る

$ mkdir app/views/shared

中身 (_error_messages.html.erb)を作成

app/views/shared/_error_messages.html.erb
<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>.
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>


これによっていろんなものが追加された

例えば、countメソッド

>>user.errors.count
=>2

any? empty? メソッド

>>user.errors.any?
=>true
>>user.errors.empty?
=>false

Rails Tutorial 7章 ユーザー登録フォーム(フォームの作成・送信)


ユーザー登録フォーム


1、form_forを使う

Railsに元々あるヘルパーメソッド。

まず、登録フォームのアクションから設定

app/controllers/users_controller.rb
def new
  @user = User.new
end


んで、これがフォーム

app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user) do |f| %>
      <%= f.label :name %>
      <%= f.text_field :name %>

      <%= f.label :email %>
      <%= f.email_field :email %>

      <%= f.label :password %>
      <%= f.password_field :password %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation %>

      <%= f.submit "Create my account", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>


2、フォームHTML


先ほど作成したフォームを理解するため、細かく見ていく。

<%= form_for(@user) do |f| %>
.
.
<% end %>

・do は、form_forが一つの変数を取るブロックを表す。
・fオブジェクトは、HTMLフォーム要素(textfieldとか)に対応するメソッドが呼び出されると、@userの属性を指定するために特別に設計されたHTMLを返す。


つまり、これでは

<%= f.label :name %>
<%= f.text_field :name %>

Userモデルのname属性を設定する、ラベル付きテキストフィールドを返す。

こんなHTMLを生成してるよ

<label for="user_name">Name</label>
<input id="user_name" name="user[name]" type="text" />




重要なのが、inputごとにあるname属性

これ

<input id="user_name" name="user[name]" - - - />
.
.
<input id="user_password" name="user[password]" - - - />


これは、Railsがnameの値を使って初期化したハッシュをparams変数経由で構成するもの。nameの値がparamsで受け取れる。んだと思う




次に、formタグ自身

<form action="/users" class="new_user" id="new_user" method="post">

action = " /users "
method = " post "  この二つが重要。
form_forはこれを勝手に生成してくれる。






3.1、ユーザー登録失敗

こんな感じで作る

createアクションでフォーム送信を受け付ける

User.newで新しいオブジェクトを作成

ユーザーを保存(または失敗)

再度、送信用のユーザー登録ページを表示





まず、createアクションを作る(途中まで)

app/controllers/users_controller.rb

def create
  @user = User,new(params[:user])
 if @user.save
   #保存の成功をここで扱う
 else
   render 'new'
 end
end

深く理解するため、デバック情報を参照してみる

"user" => { "name" => "Foo Bar",
            "email" => "foo@invalid",
            "password" => "[FILTERED]",
            "password_confirmation" => "[FILTERED]"
          }

ここでは、params[:user]に複数のハッシュが含まれている。フォーム送信の結果が、送信された値に対応する属性とともにuserハッシュに保存されている。
ハッシュのキーは、inputタグにあったnameの値。



ちなみに、この二つは同義

@user = User.new(params[:user])
@user = User.new(name: "Foo Bar", email: "foo@invalid",
                 password: "foo", password_confirmation: "bar")


けど、前者のコードではセキュリティ面で懸念があるっぽい。
そこで登場するのがStrongParameters


3.2、Strong Parameters

まず、マスアサインメントってやつ
次のコードみたいに、値のハッシュを使ってRubyの変数を初期化するもの

@user = User.new(params[:user])


マスアサインメントでは、name, emailとかの他に、actionやcontrollerといった全ての値が飛んでくる。


paramsハッシュ全体を初期化するのは危険すギル。マスアサインメント脆弱性(admin=管理者権限 があればparamsの中身を見れるし自由にシステムを操作できるっぽい)。
今は、マスアサインメントを使うとRailsが怒る。

そこで、StrongParametersを使用
これを使うことで、必須のパラメーターと許可されたパラメーターを指定することができる。って何?
 →簡単に言うと、DBへ入れたり更新したりする値を制限できるらしい


<使い方>
①requireでPOSTで受け取る値の設定
②permitで許可するカラムを設定

例えば、「user属性を必須にして、名前・アドレス・パスワード・パスワードの確認だけ更新許可したい!」ってとき

params.require(:user).permit(:name, :email, :password, :password_confirmation)


ここで、このパラメーターを使いやすくするため、巷では user_paramsとかいう外部メソッドが使われているらしい。
主に params[:user] の代わりとして使われる

@user = User.new(user_params)

これを、外部ユーザーに漏れないよう、privateに隠す

app/controllers/users_controller.rb
.
.
def create
  @uer = User.new(user_params)
  if @user.save
   #保存の成功をここで扱う
  else
   render 'new'
  end
end

private

  def user_params
    params.require(:user).permit(:name, :email, :password, :password_confirmation)
  end

Rails Tutorial 6章 パスワード編

セキュアなパスワードを追加する

手順
1、パスワードの入力、送信
2、ハッシュ化し、DBに保存
3、DB内のハッシュ化された値との比較

・ハッシュ化とは...
  ハッシュ関数をつかって不可逆なデータに変換


ハッシュ化されたパスワード

これだけ

app/models/user.rb
class User < ApplicationRecord
 .
 .
 has_secure_password
end


has_secure_passwordを追加することによって
・ハッシュ化したパスワードをDB内の password_digest という属性に保存できる
・仮想的な属性( password, password_confirmation ) が使えるようになる。また、存在値と値が一致するかのバリデーションが追加される
・authenticatedメソッドが使えるようになる

has_secure_password を使うためには、DBに password_digestが存在していなければならない

そこで、

$ rails g migration add_password_digest_to_users passsword_digest:string

上記のコード、末尾を to_users にすることによってRailsが自動的にusersテーブルにカラムを追加するマイグレーションファイルを作成する。

できたのが、これ

db/migrate/[timestamp]_add_password_digest_to_users.rb
class AddPasswordDigestToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :password_digest, :string
  end
end

んで、

$ rails db:migrate

また、has_secure_password でパスワードをハッシュ化するためには bcrypt っていうのが必要

Gemfileに追記、インストール

gem 'bcrypt',         '3.1.12'
$ bundle install


これで、has_secure_password が使えるようになった。


セキュアなパスワードの完全な実装

存在性バリデーションと最小文字数

validates :password, presence: true, length: { minimum: 6 }


ユーザーの作成と認証

>> User.create(name: "Michael Hartl", email: "mhartl@example.com",
?>             password: "foobar", password_confirmation: "foobar")

結果、

>> user = User.find_by(email: "mhartl@example.com")
>> user.password_digest
=> "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3yOcVFzb6oK"

authenticatedメソッドで、因数に渡された値をハッシュ化した値と、DB内のpassword_digestカラムの値を比較

>> user.authenticate("not_the_right_password")
false
>> user.authenticate("foobaz")
false

正しい値を与えると、そのユーザーオブジェクトを返す

>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 20:36:46", updated_at: "2016-05-23 20:36:46",
password_digest: "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3...">

ちなみに、 !! でそのオブジェクトが対応する論理値を返す

>> !!user.authenticate("foobar")
=> true