Rails Tutorial 9章　ログイン機構の自分なり解釈まとめ

#ユーザーを記憶するための手段(RailsTutorial9章)

##Cookiesメソッドを用いた永続ログインの方法
Cookiesメソッドによる、永続ログインの方法について、概要をバサッとまとめてみました。


- 永続ログインを可能にするために、ブラウザのcookieに記憶トークンと呼ばれる文字列を保存する。

- この記憶トークンをセキュアな文字列にした記憶ダイジェストをデータベースに保存する。
- この記憶トークンをデータベースのセキュアな文字列と比較し、照合する。
- 署名付き暗号化user_idもcookieに保存し、データベースのuser.idと比較する。




###記憶トークンの生成及び、ハッシュ値のデータベースへの保存
記憶トークンはSecureRandom の　urlsafe_base64を使い生成
記憶ダイジェストは

```ruby:/models/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
```
のような関数でUser.digestの引数に代入した値をBcryptでハッシュ化する。
ここでは、インスタンスメソッドで定義する必要はないので、クラスメソッドで定義している。
引数に代入する値は生成したランダムな64文字の文字列である記憶トークンを用いる。

```ruby:/models/user.rb

def User.new_token
    SecureRandom.urlsafe_base64
end
```

この二つの過程を合体させて生成した記憶トークンをハッシュ化した値をデータベースのremember_digestに保存する
この時、記憶トークンの属性も別途設定しておく

```ruby:/models/user.rb

attr_accessor :remember_token


def remember
    self.remember_token=User.new_token
    Update_attribute(:remember_digest,User.digest(remember_token))
end
```
注意 self.remember_tokenとしないとremember_tokenのローカル変数を定義してしまうことになる。

 
###current_userにログイン中のユーザー情報を保持させる

1.sessionがまず存在するかどうかを確かめる

2.ない場合はcookiesに署名されたuser_id  (つまりcookies.signed[:user_id])が存在するか確かめる

3.user=User.find_by(id:cookies.signed[:user_id])でUser モデルにcookiesのuser_idをもつユーザーが存在するか確かめる
注意　cookies.signed[:user_id]は暗号化されているが、自動的に暗号化が解除されている。

4.このuserが存在、つまりはtrueであり、かつ、そのuserのremember_digestとcookies[:remember_token]のハッシュ値が一致するとき、つまり

```ruby:
if user&& user.authenticated?(cookies[:remember_token])　・・・・・注意
@current_user=user
```

```ruby:
def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
```
としてcookiesの中のremember_tokenキーに含まれるトークンのハッシュ値が、userのremember_digestと一致するとき、
ログイン中にする。（@current_userにuserを代入する。）





1.　ー4.をまとめてコードにすると


```ruby:
def current_user
    if (user_id=session[:user_id])
        @current_user||=User.find_by(id: user_id) #@current_userが存在する場合はそのまま、ない場合はデータベースからActive Recordのfind_byメソッドを用いて検索
    elsif (user_id=cookies.signed[:user_id])
        user=User.find_by(id: user_id)
        if (user && user.authenticated?(cookies[remember_token]))#userが存在し、cookiesに保存されているトークンのハッシュ値とremember_digestが一致する場合
            @current_user=user
        end
    end
end
```
**注意：ここでuser_id=session[:user_id]とあるがこれは比較をしているのではなく、
user_idにsession[:user_id]を代入した際に存在すれば。の意味である。**

また、ヘルパーでcookiesに署名付き暗号化user.idを保存し、64文字のトークンを保存することで、
cookiesからユーザー情報をトークンで取り出せるようにする。

```ruby:app/helpers/session_helper.rb

def remember(user)
    user.remember #データベース側にremember_digestを保存
    cookies.permanent.signed[:user_id]=user.id #署名付き暗号化user_idをcookies側に保存
    cookies.permanent[:remember_token]=user.remember_token #remember_tokenをcookies側に保存
end
```
これでcurrent_userにログイン中のユーザーの記憶する手段が整った。




###ログイン時に行われるログインの永続化の処理

```ruby:/session_controller.rb

def create
    user=User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticated?(params[:session][:password])
        log_in (user) #ヘルパーで定義　#current_userにuserを代入
        remember(user)#ヘルパーで定義 #cookiesにremember_token及びuser_idを保存
        redirect_to user_url(user)
    else 
        flash.now[:danger]="Invalid email/password combination"
        render 'new'
    end
end
```

##Logoutするとき
ログアウトするときは、

- cookies.signed[:user_id]の削除
- cookies[:remember_token]の削除
- remember_digestをnilにする
- sessionの情報も破棄する
- current_userもnilにする

```ruby:models/user.rb
def forget
 update_attribute(:remember_digest,nil)
end
```

```ruby:helpers/sessions_helper.rb
def forget(user)
 user.forget
 cookies.delete(:user_id)
 cookies.delete(:remember_token)
end
```

```ruby:helpers/sessions_helper.rb
def logout
  forget(current_user)
  session.delete(:user_id)
  @current_user=nil
end
```
```ruby:helpers/sessions_helper.rb
  def destroy
    log_out if logged_in? 
    redirect_to root_url
  end
```
ここで 
```
log_out if logged_in?
```
としている理由としては
ブラウザが2つ起動していて、片方のブラウザでログアウト処理をした場合に、current_userがnilになるのでforget(current_user)の引数にしているオブジェクトがnilになるのでエラーをはくためです。

もう一つエラーがあります。
同じようにブラウザが２つ起動していて、片方でログアウト処理をしたとき、remember_digestの値がnilになるので、もう片方のブラウザで一度ブラウザを閉じて、再びつけたときに、cookieは存在するので、下の式のcookies.signed[:user_id]が評価されるので次のif文まで評価されますが、authenticated?メソッドで使っているBcrypt.newは引数であるremember_digestがnilになってしまいエラーになってしまいますので、remember_digestがnilの場合はfalseを返すようにします。

```ruby:
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])# BCryptで生成するdigestの値がnilとなりエラーが発生する。
      log_in user
      @current_user = user
    end
  end
end
```

```ruby:
def authenticated?(remember_digest)
 return false if remember_digest.nil? #remember_digestがnilの場合はfalseを返して、条件式を終了させる。
 BCrypt.new(remember_digest).is_password?(remember_token)
end
```




##まとめ

- cookiesではログインするための情報を保存する
 - 署名付き暗号化user_id
 - 記憶トークン
- ログインするときは、sessionから照合し、ない場合は、cookiesの情報を参照する。

- データベースにはハッシュ化されたトークンを保存しておき、authenticated？ヘルパーにより、cookiesの記憶トークンと照合する。
- ログアウトするときはcookiesやデータベースのremember_digestをnilにする。