MENU

問い合わせ


    【Railsで作る見積・請求管理システム 第2回】ログインを付ける(Rails 8標準の認証と権限の分け方)

    見積・請求のデータは、取引先名や金額が並ぶ社内の大事な情報です。第1回で作った取引先・品目のマスタは、今のところURLを知っていれば誰でも開けて、消せてしまいます。この第2回では、Ruby on Rails 8標準の認証ジェネレータでログインを付け、さらに「見るだけの人」「登録できる人」「削除もできる人」を分ける権限を入れます。

    前回の第1回:取引先と品目のマスタを作るの終わりで「ログインがないので、公開サーバーには置かないでください」と書きました。この回でその宿題を片づけます。

    「社内システムだから、パスワードさえあれば誰でも同じように使えれば十分では? 権限まで分ける必要はありますか?」

    先に結論です。ログインだけでは足りず、「誰が何をできるか」の線引きまで最初に決めておくのが安全です。Rails 8には認証の土台(ログイン・ログアウト・セッション管理)を作る bin/rails generate authentication が標準で入っており、外部ライブラリなしで作れます。一方で、権限の分け方は会社ごとに違うため、自分で作る部分になります。

    この記事では、認証ジェネレータの出力を読み解き、3段階の権限(閲覧のみ/担当者/管理者)を実装して、自動テストで固めるところまでを解説します。

    目次

    認証ジェネレータが作るもの、作らないもの

    Rails 8の認証ジェネレータは、ログインに必要な最小限の部品を一式つくります。公式のRails 8紹介記事によると、Session と User のモデル、SessionsController、Authentication コンセルン(=複数の画面で共有する処理のまとまり)などが生成され、一方で新規ユーザーの登録画面(サインアップ)は「アプリごとに作りが違うため」あえて含まれません。

    📰 出典:Ruby on Rails「Rails 8.0: No PaaS Required」

    生成されるものを、役割ごとに整理します。

    生成物役割
    User モデルメールアドレスとパスワード(暗号化して保存)を持つ
    Session モデル「どの端末でログイン中か」を1行ずつ記録する(IPアドレス・ブラウザ情報つき)
    Authentication コンセルン全画面で「ログイン済みか」を確認し、未ログインならログイン画面へ送る
    SessionsControllerログイン・ログアウトの処理。連続した失敗を制限する仕組み(レート制限)つき
    マイグレーションusers と sessions テーブルの作成

    なお、生成時に「パスワード再設定」の機能(メール送信つき)も一緒に作られます。この連載ではメール送信を第6回で扱うため、今回は再設定の部品を取り除いて進めます(後から足す前提です)。

    実装手順

    1. ジェネレータを実行する

    bin/rails generate authentication
    bin/rails db:migrate

    パスワードを安全に保存する has_secure_password が使う bcrypt というライブラリが、Gemfile で有効になります。これは、パスワードそのものではなく「元に戻せない形(ハッシュ)」に変換して保存する仕組みです。万一データベースが漏れても、パスワードがそのまま読まれることはありません。

    2. 全画面を「ログイン必須」にする

    app/controllers/application_controller.rb に Authentication が差し込まれます。これで、すべての画面が既定でログイン必須になります。

    class ApplicationController < ActionController::Base
      include Authentication
      include Authorization   # 次の手順で作る権限チェック
      # ...
    end

    「基本は全部ログイン必須で、開けてよい画面だけ例外にする」という向きが重要です。逆(基本は誰でも開けて、守る画面だけ指定する)だと、新しい画面を作るたびに守り忘れが起きます。ログイン画面だけが例外で、SessionsController に次の1行があります。

    # app/controllers/sessions_controller.rb
    allow_unauthenticated_access only: %i[ new create ]

    3. ユーザーに「権限」を持たせる

    users テーブルに role(権限)の列を足します。

    # db/migrate/20261002124428_add_role_to_users.rb
    class AddRoleToUsers < ActiveRecord::Migration[8.1]
      def change
        add_column :users, :role, :string, null: false, default: "viewer"
      end
    end

    既定値を「閲覧のみ(viewer)」にしているのがポイントです。権限の設定を忘れたユーザーが、うっかり強い権限を持つことを防げます。

    モデルでは、Railsの enum で3つの権限を定義し、「何ができるか」をメソッドにします。

    # app/models/user.rb
    class User < ApplicationRecord
      has_secure_password
      has_many :sessions, dependent: :destroy
    
      # viewer: 閲覧のみ / staff: 登録・更新ができる / admin: 削除もできる
      enum :role, { viewer: "viewer", staff: "staff", admin: "admin" }, default: :viewer
    
      normalizes :email_address, with: ->(e) { e.strip.downcase }
    
      validates :email_address, presence: true, uniqueness: true
      validates :password, length: { minimum: 8 }, allow_nil: true
    
      def can_edit? = staff? || admin?
      def can_destroy? = admin?
    end

    「権限の名前」ではなく「できること(can_edit?)」で判定する形にしておくと、後から「経理担当」のような権限を足しても、画面側の判定を書き換えずに済みます。

    4. 権限チェックを1か所にまとめる

    各画面に if を散らさず、コンセルンにまとめます。

    # app/controllers/concerns/authorization.rb
    module Authorization
      extend ActiveSupport::Concern
    
      class_methods do
        # 例: require_permission :can_edit?, except: %i[ index show ]
        def require_permission(permission, **options)
          before_action(**options) { forbid unless Current.user.public_send(permission) }
        end
      end
    
      private
        def forbid
          redirect_back_or_to root_path, alert: "この操作を行う権限がありません。", status: :see_other
        end
    end

    そのうえで、取引先・品目の画面に次の2行を足すだけで制限できます。

    # app/controllers/customers_controller.rb(items_controller.rb も同様)
    require_permission :can_edit?, except: %i[ index show ]
    require_permission :can_destroy?, only: :destroy

    Current.user は、リクエストごとに「いまログインしている人」を引き出すRailsの仕組み(Current)を使っています。Current モデルに delegate :user, to: :session を1行足すだけで使えるようになります。

    5. 画面も権限に合わせる

    ボタンを隠すだけでは安全になりませんが(URLを直接開かれると防げません)、押せないボタンが並ぶと使う人が混乱します。制限の本体は手順4のサーバー側で、画面の出し分けは親切のためと考えてください。

    <%# app/views/customers/index.html.erb %>
    <% if Current.user.can_edit? %><%= link_to "新規登録", new_customer_path %><% end %>

    ログイン画面は、メールアドレスとパスワードの2項目だけのシンプルな形にします。

    <%# app/views/sessions/new.html.erb %>
    <%= form_with url: session_path, class: "login-form" do |form| %>
      <p>
        <%= form.label :email_address, "メールアドレス" %>
        <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username" %>
      </p>
      <p>
        <%= form.label :password, "パスワード" %>
        <%= form.password_field :password, required: true, autocomplete: "current-password", maxlength: 72 %>
      </p>
      <%= form.submit "ログイン" %>
    <% end %>

    ログイン失敗の文言は「メールアドレスまたはパスワードが違います」とし、どちらが違うのかは教えません。「このメールアドレスは登録されていない」と分かると、第三者が登録済みのアドレスを探る手がかりになるためです。

    動作確認:自動テストで権限の線引きを固める

    権限の仕組みは、画面を目で確認するだけだと必ず抜けが出ます。そこで、「誰が何をできて、何をできないか」を自動テストにしておきます。

    # test/controllers/authorization_test.rb(抜粋)
    test "閲覧のみの権限は一覧を見られるが登録はできない" do
      sign_in_as users(:viewer)
    
      get customers_url
      assert_response :success
      assert_select "a", text: "新規登録", count: 0
    
      assert_no_difference "Customer.count" do
        post customers_url, params: { customer: { name: "新規商事" } }
      end
      assert_redirected_to root_path
    end
    
    test "担当者は登録・更新できるが削除はできない" do
      sign_in_as users(:staff)
      # ... 品目を登録できること、削除は拒否されること
    end

    ログインそのものについても、「正しい組み合わせで入れる」「パスワードが違うと入れない」「未ログインで業務画面を開くとログイン画面へ移り、ログイン後に元の画面へ戻る」をテストにしています。

    検証は次のコマンドで行い、今回の変更後は30件のテストがすべて通り、RuboCop(コードの書き方の自動チェック)も指摘なしでした。

    cd blogs/it_hacchu/series/rails-mitsumori/code
    bin/rails test && bundle exec rubocop

    ※ この回はブラウザでの目視確認ではなく、自動テストで動作を確認しています(開発環境での確認)。メール送信が必要なパスワード再設定は、第6回以降に扱うため未実装です。

    開発用の確認アカウントは db/seeds.rb に、開発環境(Rails.env.development?)の場合だけ作る形で入れています。本番環境に共通のパスワードのアカウントを残さないためで、本番の最初の管理者は、サーバー上で一度だけコマンドを実行して作る想定です(第9回で扱います)。

    つまずきやすい点・セキュリティ上の注意

    • サインアップ画面は作っていない:社内システムでは、管理者がユーザーを登録するのが一般的です。誰でも自分で登録できる形にすると、外部の人が入り込めてしまいます
    • パスワードの長さ:今回は8文字以上のチェックを入れました。maxlength: 72 は bcrypt が扱える長さの上限に合わせたものです
    • レート制限は入口の対策のひとつ:ジェネレータが入れる連続失敗の制限は有効ですが、「これで絶対に破られない」という意味ではありません。本番では、アクセスの制限やログの監視も併せて検討します
    • 権限を増やすときは、先にテストを書く:「管理者だけが使える画面」を増やすたびに、閲覧のみ・担当者で拒否されるテストを足しておくと、守り忘れに気づけます
    • この連載で省略していること:二段階認証、パスワード再設定、ログイン履歴の画面、アカウントのロックなどは扱いません。必要な場合は別途の費用になります

    発注者向けメモ:ログインと権限を頼むときの確認点

    ログイン機能は「どこまで作るか」で費用が大きく変わります。開発会社に頼むときは、次を確認してください。

    • ☐ 誰がシステムを使うか(社員だけか、取引先も入るか)を一覧にして伝えたか
    • ☐ 権限の種類(見るだけ/登録できる/削除できる/承認できる など)を、実際の業務に沿って決めたか
    • ☐ ユーザーの追加・退職者の無効化を、誰が・どの画面でするか決めたか
    • ☐ パスワードを忘れたときの再設定の流れ(メール送信を含む)が見積に入っているか
    • ☐ 二段階認証やシングルサインオン(=会社の共通IDでログインする仕組み)が必要か、必要なら見積に含まれているか
    • ☐ 「いつ・誰が・何を変更したか」の記録(操作ログ)が必要か

    開発会社への質問例

    • 「権限は何段階を想定していますか。見るだけの人と、削除できる人を分けられますか」
    • 「ログイン機能は自社で作りますか、既存の仕組み(認証サービスなど)を使いますか。それぞれの運用上の違いは何ですか」
    • 「退職者のアカウントを止める手順は、画面からできますか。それとも開発会社への依頼が必要ですか」
    • 「ログインに失敗し続けた場合や、パスワードが漏れた場合の対策は、どこまで含まれていますか」

    工数が増えやすいのは、権限の種類が多い場合と、取引先の画面ごとに見せる範囲が違う場合(「A社の担当者はA社のデータだけ見える」など)、そして二段階認証やシングルサインオンです。最初のリリースでは3段階程度の権限で始め、使いながら増やす進め方も現実的です。

    「権限の分け方は、あとから変えると画面もテストも直すことになります。最初に『誰が何をするか』を一緒に書き出しておくのが、いちばん安くつきます」

    まとめと次回予告

    第2回では、Rails 8標準の認証ジェネレータでログインを付け、権限(閲覧のみ/担当者/管理者)をサーバー側で制限し、自動テストで線引きを固めました。外部のライブラリやサービスを使わずに、業務システムに必要な入口の守りの土台ができました。

    次回の第3回は「見積書と明細を作る」です。見積書(親)と明細行(子)の関係をモデルで表し、Hotwire(=画面の一部だけを書き換える仕組み)で、明細行をその場で追加できる画面を作ります。

    この連載の記事一覧

    この記事は連載「Railsで作る見積・請求管理システム」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。

    システム制作・運用・保守のお問い合わせはこちら


      よかったらシェアしてね!
      • URLをコピーしました!
      • URLをコピーしました!

      この記事を書いた人

      株式会社THIRD HERO代表取締役 朝野貴朗
      Webシステム開発を中心に、toC向けサービスサイトの運営、ツール開発などを行ってまいりました。

      コメント

      コメント一覧 (2件)

      目次