Railsでログイン機能を自作する

はじめに

deviseを使いたくないし、と思ったらsorceryは開発終了してしまってるみたいだし、monbanはきになるけどproductionに載せるには少し不安だ...。

ということで、僕がきいてる限りだと大きな会社のアプリケーションだとかなり自作の事例の方が多いみたいなので、もう自作してしまおうと思いました。その方が勉強にもなりそうだし。

Rails Tutorialでログイン機構の自作部分があったのを思い出したので、そこを参考にします。

http://railstutorial.jp/chapters/log_in_log_out?version=4.2#cha-log_in_log_out

今回のログイン機構では、ブラウザに保存される小さなテキストデータであるcookieを使用してsessionの管理をします。(たいていユーザログイン機能はこの形じゃないかなぁと思います)

Railsのsessionとcookiesメソッドを使っていきます。

大事な方針として、「セッションをRESTfulなリソースとしてモデリングする」という部分。セッションを、他のリソースと同じような形式で、統一的に扱うことができるわけですね。(Rails' Wayに乗せやすいということでもある。)

具体的に説明すると、newで新しいセッションを作成して、そこでログインするとcreateでセッションを保存、ログアウトはdestroyでセッションを破棄する、って感じ。ActiveRecordのデータベースの代わりに、ブラウザのcookie領域がデータの操作先になるというイメージ。なるほど。

つくってみる

Userモデルの作成

Userモデルを作成します。Rails Tutorialの以下の部分を参考に。

http://railstutorial.jp/chapters/modeling_users?version=4.2#sec-user_model

passwordは、Railsのhas_secure_passwordの機能を用いて管理します。

先に、パスワードの管理にハッシュ化を用いるため、Gemfileにbcryptを追加(コメントイン)します。(password_digest属性を持つモデルクラスを作成しようとすると自動で警告してくれます!すご)

gem 'bcrypt'

そしたら以下のように、has_secure_password機能を用いるための条件であるpassword_digest属性を持ったモデルクラスを作成します。

$ bin/rails g model User email:string password_digest:string

Userモデルで、has_secure_passwordメソッドを呼びます。

class User < ActiveRecord::Base
  .
  .
  .
  has_secure_password
end

モデルクラス内でこのメソッドを読んでおくと、モデルクラスに以下のような機能が追加されます。

セキュアにハッシュ化したパスワードを、データベース内のpassword_digestという属性に保存できるようになる。 2つのペアの仮想的な属性18 (passwordとpassword_confirmation)が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される。 authenticateメソッドが使えるようになる (引数の文字列がパスワードと一致するとUserオブジェクトを、間違っているとfalse返すメソッド)。

ログイン機構の作成

viewが必要なのはnewアクションだけなので、以下のようにSessionsControllerを作成する。

$ bin/rails g controller Sessions new

以下のようにroutingに加えます。

Rails.application.routes.draw do
  ...
  get    'login'   => 'sessions#new'
  post   'login'   => 'sessions#create'
  delete 'logout'  => 'sessions#destroy'
end

セッションの維持の処理を書きます。

class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザログイン後にadminページに遷移する
    else
      flash.now[:danger] = 'emailまたはpasswordが間違っています'
      render "new"
    end
  end

  def destroy
  end
end

SessionsHelperとうモジュールをApplicationControllerにincludeしておきます。

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

SessionsHelperにlog_inメソッドを作成します。

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

session[:hoge]に値を代入すると、ブラウザ内のcookieに暗号化済みの値を自動的に作成してくれます。便利。cookieメソッドの場合は、sessionメソッドと違ってブラウザを閉じた瞬間に消えます。

これで、以下のようにすればloginの機能が一応完成します。

class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_to admin_path
    else
      flash.now[:danger] = 'emailまたはpasswordが間違っています'
      render "new"
    end
  end

  def destroy
  end
end

その他の便利メソッド

そのほかの、必ず使う便利なヘルパーメソッドもつくっておきます。

current_userメソッドも作成しておきましょう。

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

ここでポイントなのですが、単にUser.find_by...ってUserモデルからsession情報に基づいてuserインスタンスを引っ張ってくるのではなく、@current_userが入ってるかどうかを確認しましょう。Railsでの重要かつ基本的な高速化の方法ですね。

続いてユーザがログインしてるかどうかを真偽値で返してくれるloged_in?もつくっておきましょう。

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

ログアウト機能の実装

log_outメソッドを作成します。sessionを削除するように実装します。@current_usernilにしておきます。

module SessionsHelper
  ...
  
  def logout
    session.delete(:user_id)
    @current_user = nil
  end
end

destoryメソッドを実装。

class SessionsController < ApplicationController
  ...

  def destroy
    log_out
    redirect_to root_url
  end
end

次回に続く

ここまでで、ログイン機能の基本的な部分はできました。

ですがこのままだとブラウザを閉じるとセッションの情報が失われてしまいます。

次回の記事で、このセッションを維持する機能を作成していきます。

【動的VS静的】Railsの404/500エラーページ 静的の勝利

揉めに揉める議論

最初、もりじゅんさんの以下の記事を参考にさせていただこうとしたのですが、

morizyun.github.io

Qiitaでも(多分もりじゅんさんの記事を参考にした)同じ例をみかけて、そこでyuki24さんが、この方法はあまりよい方法でないというコメントをしてました。

Railsの404,500エラーページをカスタマイズ - Qiita

このコメントでひと記事できてしまうやつ。。笑

必読です。。

ということでApplicationControllerに例外ハンドラを書いてそれに応じてrenderするtemplateを振り分ける、という方法はできれば避けたいですね。

Rails覚えたての頃は、そちらの方法で実装していました。

totutotu.hatenablog.com

totutotu.hatenablog.com

実際に最初に静的ページで実装してみると、もともとRailsで用意されているpublic/hoge.htmlを使うことになるとは思いますが、エラーページにナビゲーションバーやフッターを追加してその内容が変わっていくのであれば、あまり賢くないように思います。いちいち変更するのはとても面倒そうです。

そこでRailsconfig.exceptions_appという設定があり、それをうまく使うことでエラー処理を行うことが出来るのでそれを使おう、ということになります。

rambulanceを試してみる

この方法を用いている、yuki24さんが作られているgemを使いながらこの方法についてみていきたい。

github.com

まずはgemを追加してbundle install

# Gemfile
gem 'rambulance'
$ bundle install

そしたらranbulance:installでファイルのテンプレートを生成します。-eオプションで特定のテンプレートエンジンを指定できます。

$ rails g rambulance:install -e slim

Railsではproductionで動かしてもアドレスがlocalhost127.0.0.1だと本番用のエラーページを出してくれません。ranbulanceはその辺も対応してくださってます。素敵すぎる。。

以下の様なURLでエラーページにアクセスすることができます。

localhost:3000/rambulance/***

ここまで使い進めてやはりつまずいたのは、exceptions_appを用いているというところ。。

exceptions_appは3.2から追加された機能で、例外発生時に呼ばれる例外処理アプリケーションを設定するというもの。デフォルトではActionDispatch::PublicExceptions.new(Rails.public_path)が使用される。Release Noteからは以下。

Added config.exceptions_app to set the exceptions application invoked by the ShowException middleware when an exception happens. Defaults to ActionDispatch::PublicExceptions.new(Rails.public_path).

参考: Railsのエラーハンドリング - 一分一秒真剣勝負!

こいつでRails標準のActionDispatch::PublicExceptionsを独自のコントローラに差し替えることができます。

middlewareレベルでExceptionsをrescueするActionDispatch::PublicExceptionsクラスのコードは以下。読むととても勉強になります。

rails/show_exceptions.rb at f49d20ef36c2d339e7a988fdc52981cdb95af22f · rails/rails · GitHub

ですが、例外が発生した場合はApplicationControllerまで届かずActionDispach::PublicExceptionsにrescueされてそちらのcontrollerに処理が委託されるので、ApplicationControllerで定義しているインスタンスメソッドやincludeしているApplicationHelperなどのヘルパーメソッドは使えなくなってしまう。

僕の場合はspのviewのlayout切り替えをここで行っていたりstylesheetの読み込み用のメソッドを用意したりしていたので結構つらみがあった。

やっぱり動的にやるのきつい。。

yuki24さんもおっしゃるように、よほど動的にしないといけないケース以外では静的ページとしてエラーページを用意する方が今のところよさそうかなと思いました。。結局メンテナンスのときなど、HTTPサーバーのレイヤーからエラーを出さなければいけないときは別途対応が必要ですし。

この方の記事も読んでおくとRailsでのエラー処理の考えが深まります。。

kami30k.com

雑に学んだことをまとめると、ApplicationControllerで拾った例外でview振り分けるのでは(ActionController::RoutingError は ApplicationControllerに到達する前に例外が発生しているので)見た目の解決にしかなってないのでよくなくて、やるならActionDispatch::PublicExceptionsで委託するcontrollerを変えないといけないのだけど、それだとApplicationController経由しないから色々つらいよ、みたいな感じ。。。

yuki24さんの結論は「そこまでして動的にやる意味あるんですかね?」とのこと。。。w

参考: Railsでエラーページを動的に - Qiita

つらい

ということで結局静的ページでやることにしました。つらい

pg_dumpの実行時にパスワード入力を省略する

pgではpg_dump実行時に必ずインタラクティブなパスワードの入力を求められるのですが、cron jobでのpb_dump実行時に、パスワードの入力を省きたいって感じのことです。どうやらパスワードを渡す方法はないみたいですね。

PostgreSQL公式docのpg_dumpのところ

-wオプションのところで以下のような記述が。

-w –no-password パスワードの入力を促しません。 サーバがパスワード認証を必要とし、かつ、.pgpassファイルなどの他の方法が利用できない場合、接続試行は失敗します。 バッチジョブやパスワードを入力するユーザが存在しない場合にこのオプションは有用かもしれません。

ふむ、.pgpassなるファイルがあるのか。

d.hatena.ne.jp

公式doc

31.15. パスワードファイル

.pgpassというファイルをホームディレクトリに配置することで、pgは勝手にそれをパスワードとして使用してくれるみたいです。

.pgpassのフォーマットは以下のようにします。

hostname:port:database:username:password

最初の4フィールドに関してはワイルドカードもありみたいです。また、

Unixシステムにおいて、.pgpassの権限はグループ、他者へのアクセスをすべて拒否しなければなりません。 これはchmod 0600 ~/.pgpassといったコマンドによって行います。 権限をこれよりも緩くすると、このファイルは無視されます。

とのことなので、

$ sudo chmod 0600 ~/.pgpass

としておきましょう。

これで、

$ pg_dump -Ft -U USER_NAME -w DB_NAME > BACKUP_FILE

のように-wでパスワードを省略してpd_dumpを実行することができました。わーい