読者です 読者をやめる 読者になる 読者になる

ztbuz@dev

人生に絶望しているZが、それでも技術を身につけようと必死になるブログ

Ruby on Railsでユーザ登録/ログイン機能を自作する方法

先日Ruby on Railsチュートリアルを完了しました。 疲れた。。。

それで、ちょっとごちゃごちゃしてきたので、ユーザ登録〜ログイン機能までの実装のみを抜き出して自分なりに整理してみました。

ちなみに今回長くなるのでバリデーションやテストは省略しています。

準備

ディレクトリ構成

編集するファイルは以下のような感じです。

app/
  controllers/
    application_controller.rb
    sessions_controller.rb
    users_controller.rb
  helpers/
    sessions_helper.rb
  models/
    user.rb
  views/
    layouts/
      application.html.erb
    sessions/
      new.html.erb
    users/
      edit.html.erb
      index.html.erb
      new.html.erb
      show.html.erb
config/
  routes.rb

テーブル

テーブルは以下のようにUserモデルのみ利用します。 ちなみにイカの頭以下の図はRails ERDを用いて生成しました(なんつー誤字だ……)。

f:id:ztbuz:20131229090804p:plain

上記を準備するコマンド

とりあえずパパパパーンッと上記を準備します。

$ rails new Login
$ cd Login
$ rails g controller Sessions
$ rails g controller Users
$ rails g model User name:string email:string password_digest:string remember_token:string
$ touch app/views/sessions/new.html.erb
$ touch app/views/users/edit.html.erb
$ touch app/views/users/index.html.erb
$ touch app/views/users/show.html.erb
$ touch app/views/users/new.html.erb
$ bundle exec rake db:migrate
$ vi Gemfile # bcrypt-ruby をコメントアウト
$ bundle install

各ファイル

各ファイルの中身と、コメントをちらほら記します。 それではディレクトリ構成の上から順に(適当)。

application_controller.rb

コントローラ全体からSessionsHelperを呼び出すことになるので、ここでこれをincludeしておきます。 以上。

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

sessions_controller.rb

Sessionコントローラはその名のとおりセッションを司ります。 RESTな考えでいくと、newはログインフォーム、createはログイン処理、destroyはログアウトですね。

class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      sign_in user
      redirect_to user
    else
      render 'new'
    end
  end

  def destroy
    sign_out
    redirect_to root_url
  end
end

users_controller.rb

こちらはよくあるUserコントローラです。 今回はeditupdateのみ権限を課しているので、before_actionでそれっぽいことをします。

class UsersController < ApplicationController
  before_action :signed_in_user, only: [:edit, :update]
  before_action :correct_user, only: [:edit, :update]

  def index
    @users = User.all
  end

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def edit
  end

  def create
    @user = User.new(user_params)
    if @user.save
      sign_in @user
      redirect_to @user
    else
      render 'new'
    end
  end

  def update
    if @user.update_attributes(user_params)
      redirect_to @user
    else
      render 'edit'
    end
  end

  private

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

    def correct_user
      @user = User.find(params[:id])
      redirect_to root_url unless current_user?(@user)
    end
end

sessions_helper.rb

今回のメインっぽい部分。 ログイン時にトークンを更新して、以降は該当するトークンをfindしてcurrent_userにセットします。

この辺でインスタンスメソッドとクラスメソッドの理解がだいぶ深まりました。。。

module SessionsHelper
  def sign_in(user)
    remember_token = User.new_remember_token
    cookies.permanent[:remember_token] = remember_token
    user.update_attribute(:remember_token, User.encrypt(remember_token))
    self.current_user = user
  end

  def sign_out
    self.current_user = nil
    cookies.delete(:remember_token)
  end

  def current_user=(user)
    @current_user = user
  end

  def current_user
    remember_token = User.encrypt(cookies[:remember_token])
    @current_user ||= User.find_by(remember_token: remember_token)
  end

  def current_user?(user)
    user == current_user
  end

  def signed_in?
    !current_user.nil?
  end

  def signed_in_user
    redirect_to signin_url unless signed_in?
  end
end

user.rb

Userモデル。 このhas_secure_passwordがいろいろよしなにやってくれます。 というかやってくれ過ぎでしょ。。。

後ろ2つはインスタンスに関係ない部分なのでクラスメソッドとして実装しています。

class User < ActiveRecord::Base
  before_save { email.downcase! }

  has_secure_password

  def self.new_remember_token
    SecureRandom.urlsafe_base64
  end

  def self.encrypt(token)
    Digest::SHA1.hexdigest(token.to_s)
  end
end

application.html.erb

ここからはただのViewです。 application_controller.rbSessionsHelperincludeしたので、signed_in?で便利に振り分けることができます。

<!DOCTYPE html>
<html>
<head>
  <title>Login</title>
  <%= stylesheet_link_tag    "application", media: "all", "data-turbolinks-track" => true %>
  <%= javascript_include_tag "application", "data-turbolinks-track" => true %>
  <%= csrf_meta_tags %>
</head>
<body>

<ul>
  <li><%= link_to 'ユーザ一覧', users_path %></li>
  <% if signed_in? %>
  <li><%= link_to 'プロフィール', current_user %></li>
  <li><%= link_to 'ログアウト', signout_path, method: :delete %></li>
  <% else %>
  <li><%= link_to 'ログイン', signin_path %></li>
  <li><%= link_to 'アカウント作成', signup_path %></li>
  <% end %>
</ul>

<hr>

<%= yield %>

</body>
</html>

sessions/new.html.erb

何の変哲もないViewです。 Sessionはモデルがないので、form_forがちょっと特殊です。

<h1>ログイン</h1>

<%= form_for(:session, url: sessions_path) do |f| %>

  <table border="1">
    <tr>
      <th><%= f.label :email, 'メールアドレス' %></th>
      <td><%= f.text_field :email %></td>
    </tr>
    <tr>
      <th><%= f.label :password, 'パスワード' %></th>
      <td><%= f.password_field :password %></td>
    </tr>
  </table>

  <%= f.submit 'ログインする' %>

<% end %>

users/edit.html.erb

これもただのViewです。

<h1>アカウント編集</h1>

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

  <table border="1">
    <tr>
      <th><%= f.label :name, '名前' %></th>
      <td><%= f.text_field :name %></td>
    </tr>
    <tr>
      <th><%= f.label :email, 'メールアドレス' %></th>
      <td><%= f.text_field :email %></td>
    </tr>
    <tr>
      <th><%= f.label :password, 'パスワード' %></th>
      <td><%= f.password_field :password %></td>
    </tr>
    <tr>
      <th><%= f.label :password_confirmation, '確認' %></th>
      <td><%= f.password_field :password_confirmation %></td>
    </tr>
  </table>

  <%= f.submit 'アカウントを編集する' %>

<% end %>

users/index.html.erb

ただ一覧を表示するだけです。 link_touserを渡すとshowしてくれるらしいです。 この辺のRailsのノリについていけない。。。

<h1>ユーザ一覧</h1>

<ul>
  <% @users.each do |user| %>
  <li><%= link_to user.name, user %></li>
  <% end %>
</ul>

users/new.html.erb

そのうちPartialの記事も書きます。。。

<h1>アカウント作成</h1>

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

  <table border="1">
    <tr>
      <th><%= f.label :name, '名前' %></th>
      <td><%= f.text_field :name %></td>
    </tr>
    <tr>
      <th><%= f.label :email, 'メールアドレス' %></th>
      <td><%= f.text_field :email %></td>
    </tr>
    <tr>
      <th><%= f.label :password, 'パスワード' %></th>
      <td><%= f.password_field :password %></td>
    </tr>
    <tr>
      <th><%= f.label :password_confirmation, '確認' %></th>
      <td><%= f.password_field :password_confirmation %></td>
    </tr>
  </table>

  <%= f.submit 'アカウントを作成する' %>

<% end %>

users/show.html.erb

自分のプロフィールページのみ、編集リンクを表示するようにします。

<h1>ユーザ情報</h1>

<table border="1">
  <tr>
    <th>名前</th>
    <td><%= @user.name %></td>
  </tr>
  <tr>
    <th>メールアドレス</th>
    <td><%= @user.email %></td>
  </tr>
</table>

<% if current_user?(@user) %>
<p><%= link_to '編集', edit_user_path %></p>
<% end %>

routes.rb

この辺は無抵抗に書きます。

Login::Application.routes.draw do
  resources :users, only: [:index, :show, :new, :edit, :create, :update]
  resources :sessions, only: [:new, :create, :destroy]

  match 'signup', to: 'users#new', via: 'get'
  match 'signin', to: 'sessions#new', via: 'get'
  match 'signout', to: 'sessions#destroy', via: 'delete'

  root 'users#index'
end

おわりに

以上です。 便利なGemにdeviseとかcancanとかあるらしいですが、私みたいなシロウトはまず自分で書いて理解しなければならないと思うのです。。。