deviceを使わないでログイン認証機能を作ってみる

※この記事はRails初学者向けです



:::note info
deviceってなに？
railsのgemの一つで、ログイン認証機能が簡単に実装できる。
[https://github.com/heartcombo/devise](https://github.com/heartcombo/devise)
:::

gemは便利だけど、勉強のためにログイン認証周りの仕組みに興味があったので実装してみました。view周りの実装は今回の趣旨とは異なるため触れてません。
## モデル・コントローラの準備

### 1. Userモデルを作成して、schemaファイルを修正

```rb
$ rails g model user name:string email:string encrypted_password:string
      invoke  active_record
      create    db/migrate/[timestamp]_create_users.rb
      create    app/models/user.rb
      invoke    rspec
      create      spec/models/user_spec.rb
```

- **db/migrate/[timestamp]_create_users.rb** に追記

```rb
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.string :email, null: false
      t.string :encrypted_password, null: false

      t.timestamps
      t.index :email, unique: true
    end
  end
end
```
ログイン時にemailからユーザを特定するため、indexを貼って検索速度を上げ、
`unique: true`としてメールアドレスの一意性が保たれるようにします。

追記できたら以下を実行

```rb
$ rails db:migrate
== [timestamp] CreateUsers: migrating ==================================
-- create_table(:users)
   -> 0.0420s
== [timestamp] CreateUsers: migrated (0.0421s) =========================
```
```rb
$ rails db:migrate:redo
== [timestamp] CreateUsers: reverting ==================================
-- drop_table(:users)
   -> 0.0083s
== [timestamp] CreateUsers: reverted (0.0176s) =========================

== [timestamp] CreateUsers: migrating ==================================
-- create_table(:users)
   -> 0.0140s
== [timestamp] CreateUsers: migrated (0.0141s) =========================
```

マイグレーションは1つ前の状態に戻せることを担保できていることが大切なので
`rails db:migrate:redo`を流して確認する癖をつけましょう。

### 2. Userの新規登録機能の実装

Usersコントローラを作成

```rb
$ rails g controller Users new create
      create  app/controllers/users_controller.rb
       route  get 'users/new'
              get 'users/create'
      (省略)
```

<br>
ルーティングを修正

- **config/routes.rb**に追記

```rb
Rails.application.routes.draw do
  (省略)
  resources :users, only: [:new, :create] 
end
```


<br>
Userモデルにバリデーションやコールバックを実装

https://railsguides.jp/active_record_callbacks.html#%E3%82%B3%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF%E3%81%AE%E6%A6%82%E8%A6%81

- **app/models/user.rb**に追記

```rb
class User < ApplicationRecord
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

  attribute :password, :string

  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  validates :encrypted_password, presence: true, length: { is: 128 }
  validates :password, presence: true, length: { minimum: 6 }

  after_initialize do |user|
    user.encrypted_password = Digest::SHA512.hexdigest(user.password) if user.password
  end

  before_save :downcase_email!

  private

  def downcase_email!
    email.downcase!
  end
end
```



`after_initialize`コールバックの中で、ユーザ登録される際、view側から`password`として受け取り、それをハッシュ化（データを不規則な文字列に変換する手法）して`encrypted_password`に渡しています。

https://docs.ruby-lang.org/ja/latest/method/Digest=3a=3aBase/i/hexdigest.html


http://localhost:3000/users/new にアクセスし、
ユーザが登録できることを確認しましょう。
![スクリーンショット 2022-08-28 9.10.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857784/0fdd5768-2682-46a5-5c4d-a90ea310e001.png)
![スクリーンショット 2022-08-28 9.14.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857784/1573e56b-2b44-c7c8-ec92-a95a5f3a9dd2.png)



```rb
$ rails c
Loading development environment (Rails 7.0.3.1)
irb(main):001:0> User.all
  User Load (8.4ms)  SELECT `users`.* FROM `users`
=>
[#<User:0x000000011668b598
  id: 1,
  name: "test_user",
  email: "test@example.com",
  encrypted_password: "[FILTERED]",
  (省略)
```
登録できていそうですね。
次はログインログアウトの機能を追加していきます。

### 3. ログイン・ログアウト機能の実装

sessionsコントローラを作成します。
```rb
$ rails g controller Sessions new create destroy
      create  app/controllers/sessions_controller.rb
       route  get 'sessions/new'
              get 'sessions/create'
              get 'sessions/destroy'
      (省略)
```

<br>
ルーティングを修正

- **config/routes.rb**に追記

```rb
Rails.application.routes.draw do
  (省略)
  resources :users, only: [:new, :create] 
  get :login, to: 'sessions#new'
  post :login, to: 'sessions#create'
  delete :logout, to: 'sessions#destroy'
end
```

pathを分かりやすくするために `login`と`logout`に設定しました。
続いてコントローラの中身を実装していきたいのですが、その前にControllerやviewで使いたいので、ヘルパーを準備しておきます。

- **app/helpers/sessions_helper.rb** に追記

```rb
module SessionsHelper
  def log_in(user)
    session[:user_id] = user.id
  end

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

  def log_out
    reset_session
  end
end
```

sessions_helperをapplication_controllerでincludeしておきます。
- **app/controllers/application_controller.rb**に追記

```rb
class ApplicationController < ActionController::Base
  include SessionsHelper
end
```


- **app/controllers/sessions_controller.rb**に追記
```rb
class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: params[:email].downcase)
    if user && Digest::SHA512.hexdigest(params[:password]) == user.encrypted_password
      login
      flash[:success] = 'ログインが成功しました'
      redirect_to root_path
    else
      flash[:danger] = 'メールアドレス・またはパスワードが正しくありません'
      render :new
    end
  end

  def destroy
    reset_session
    redirect_to root_path
  end
end
```

ここでやってるのは、先にUserモデルから`params[:email]`に一致するユーザを検索。ユーザが見つかり、かつ`user.encrypted_password`と`params[:password]`をハッシュ化した値が一致すれば`session[:user_id]`に`user_id`を入れています。


http://localhost:3000/login にアクセスしログインしてみましょう。
![スクリーンショット 2022-08-28 9.28.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857784/bb0675d0-a29c-564c-007c-89d6cde6d97f.png)
![スクリーンショット 2022-08-28 9.29.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857784/30008a4f-35e8-e5b1-3a2a-297f8bdc29c3.png)

これでログインできました。
ログイン状態ならログアウトボタン、ログアウト状態ならユーザ登録とログインボタンを表示させてみましょう。
今回はテスト用にヘッダーに表示させています。

- **app/views/layouts/_header.html.erb**に追記

```erb
<header class="navbar navbar-fixed-top">
 <div class="container">
   <%= link_to(image_tag("", :alt => ""), root_url, id: 'logo') %>
   <nav>
     <ul class="nav navbar-nav navbar-left">
       <% if current_user %>
        <li class="navbar-item"><%= button_to "ログアウト", logout_path, method: :delete %></li>
      <% else %>
        <li class="navbar-item"><%= link_to "ユーザー登録", new_user_path %></li>
        <li class="navbar-item"><%= link_to "ログイン", login_path %></li>
      <% end %>
     </ul>
   </nav>
 </div>
</header>
```

- ログイン状態
![スクリーンショット 2022-08-28 10.08.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857784/e2916f06-8806-39f8-1aea-d5c219ebdc08.png)

- ログアウト状態
![スクリーンショット 2022-08-28 10.10.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857784/d2c3cea1-258c-3238-8dc3-60c24a7707a9.png)

うまく実装できていそうです。
`current_user`でログイン状態が分かるようになったので、別の機能の実装する際も、ログインしていなければリダイレクトさせるようなコールバックにも使えますね。

gemを使わずにログイン周りの機能を実装できました。
これで以上になります！