💎

Rails api mode login

2024/09/15に公開

Rails 7 API モード:認証と管理機能の実装

Railsを昔勉強していたときは、erbテンプレートエンジンでログインをしておりました。しかし現代では、ViewはReact.jsVue.jsで作成して、MVCといいながらコントローラーとモデルしか使いません😅
データベースはaqliteを使用しています。

Githubのコミット履歴載せておきますこちら必要であれば参考にしてみてください。

1. プロジェクトのセットアップ

プロジェクトと名はなんでも良いです。

rails new your_app_name --api
cd your_app_name

2. 必要なgemの追加

Gemfileに以下を追加:

gem 'bcrypt', '~> 3.1.7'
gem 'jwt'

そして、以下を実行:

bundle install

3. ユーザーモデルの作成

rails generate model User email:string password_digest:string
rails generate migration AddAdminToUsers admin:boolean
rails db:migrate

4. ユーザーモデルの設定

app/models/user.rb:

class User < ApplicationRecord
  has_secure_password
  validates :email, presence: true, uniqueness: true
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, length: { minimum: 6 }, if: -> { new_record? || !password.nil? }

  def admin?
    admin
  end
end

5. JWT用のサービスクラスの作成

app/services/json_web_token.rb:

class JsonWebToken
  SECRET_KEY = Rails.application.secrets.secret_key_base.to_s

  def self.encode(payload, exp = 24.hours.from_now)
    payload[:exp] = exp.to_i
    JWT.encode(payload, SECRET_KEY)
  end

  def self.decode(token)
    decoded = JWT.decode(token, SECRET_KEY)[0]
    HashWithIndifferentAccess.new decoded
  end
end

6. 認証コントローラーの作成

app/controllers/authentication_controller.rb:

class AuthenticationController < ApplicationController
  skip_before_action :authenticate_request

  def login
    @user = User.find_by_email(params[:email])
    if @user&.authenticate(params[:password])
      token = JsonWebToken.encode(user_id: @user.id)
      time = Time.now + 24.hours.to_i
      render json: { token: token, exp: time.strftime("%m-%d-%Y %H:%M"),
                     email: @user.email }, status: :ok
    else
      render json: { error: 'unauthorized' }, status: :unauthorized
    end
  end
end

7. アプリケーションコントローラーの更新

app/controllers/application_controller.rb:

class ApplicationController < ActionController::API
  before_action :authenticate_request
  attr_reader :current_user

  private

  def authenticate_request
    header = request.headers['Authorization']
    header = header.split(' ').last if header
    begin
      @decoded = JsonWebToken.decode(header)
      @current_user = User.find(@decoded[:user_id])
    rescue ActiveRecord::RecordNotFound => e
      render json: { errors: e.message }, status: :unauthorized
    rescue JWT::DecodeError => e
      render json: { errors: e.message }, status: :unauthorized
    end
  end
end

8. ユーザー登録コントローラーの作成

app/controllers/users_controller.rb:

class UsersController < ApplicationController
  skip_before_action :authenticate_request, only: [:create]
  
  def create
    @user = User.new(user_params)
    if @user.save
      token = JsonWebToken.encode(user_id: @user.id)
      time = Time.now + 24.hours.to_i
      render json: { token: token, exp: time.strftime("%m-%d-%Y %H:%M"),
                     email: @user.email }, status: :created
    else
      render json: { errors: @user.errors.full_messages },
             status: :unprocessable_entity
    end
  end

  private

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

9. 管理者用コントローラーの作成

app/controllers/admin_controller.rb:

class AdminController < ApplicationController
  before_action :ensure_admin

  def users
    @users = User.all
    render json: @users, status: :ok
  end

  def update_user
    @user = User.find(params[:id])
    if @user.update(user_params)
      render json: @user, status: :ok
    else
      render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
    end
  end

  def delete_user
    @user = User.find(params[:id])
    @user.destroy
    head :no_content
  end

  private

  def ensure_admin
    render json: { error: 'Unauthorized' }, status: :unauthorized unless current_user.admin?
  end

  def user_params
    params.permit(:email, :password, :password_confirmation, :admin)
  end
end

10. ルーティングの設定

config/routes.rb:

Rails.application.routes.draw do
  post '/auth/login', to: 'authentication#login'
  post '/signup', to: 'users#create'

  namespace :admin do
    get '/users', to: 'admin#users'
    put '/users/:id', to: 'admin#update_user'
    delete '/users/:id', to: 'admin#delete_user'
  end
end

11. 使用方法

ユーザー登録

POST /signup
{
  "email": "user@example.com",
  "password": "password123",
  "password_confirmation": "password123"
}

ログイン

POST /auth/login
{
  "email": "user@example.com",
  "password": "password123"
}

管理者用エンドポイント

  • ユーザー一覧取得:GET /admin/users
  • ユーザー情報更新:PUT /admin/users/:id
  • ユーザー削除:DELETE /admin/users/:id

※ これらのエンドポイントには管理者権限を持つユーザーのみがアクセス可能です。

12. 注意点

  • この実装は基本的な機能のみを提供しています。実際のアプリケーションではさらなるセキュリティ対策が必要です。
  • 管理者権限の付与は現在、直接データベースで行う必要があります。
  • フロントエンド(例:React, Vue.js)で管理画面のUIを別途実装する必要があります。
  • 本番環境では必ずHTTPS通信を使用し、適切なCORS設定を行ってください。
  • より強固なセキュリティのために、以下の実装も検討してください:
    • 強力なパスワードポリシー
    • アカウントロックアウト機能
    • 二要素認証
    • APIレートリミット

この設定で、Rails 7 API モードでの基本的な認証システムと管理機能が実装されます。必要に応じて、これらの基本機能をカスタマイズや拡張してください。

curlでログインをしてみる

HTTP POSTしないとできるはずがないな。やってみよう。POSTMAN持ってる人いたらそちらがいいと思いますので使ってください。

curl -X POST http://localhost:3000/signup \
     -H "Content-Type: application/json" \
     -d '{"email": "user@example.com", "password": "password123", "password_confirmation": "password123"}'

error code

hashimotojunichi@hashimotojunichinoMacBook-Pro icchy_api % curl -X POST http://localhost:3000/signup
-H "Content-Type: application/json"
-d '{"email": "user@example.com", "password": "password123", "password_confirmation": "password123"}'
{"status":500,"error":"Internal Server Error","exception":"#\u003cNoMethodError: undefined method secrets' for #\u003cIcchyApi::Application\u003e\u003e","traces":{"Application Trace":[{"exception_object_id":18240,"id":0,"trace":"app/services/json_web_token.rb:2:in \u003cclass:JsonWebToken\u003e'"},{"exception_object_id":18240,"id":1,"trace":"app/services/json_web_token.rb:1:in \u003cmain\u003e'"},{"exception_object_id":18240,"id":6,"trace":"app/controllers/users_controller.rb:7:in create'"}],"Framework Trace":[{"exception_object_id":18240,"id":2,"trace":"\u003cinternal:/Users/hashimotojunichi/.rbenv/versions/3.2.5/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb\u003e:38:in require'"},{"exception_object_id":18240,"id":3,"trace":"\u003cinternal:/Users/hashimotojunichi/.rbenv/versions/3.2.5/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb\u003e:38:in require'"},{"exception_object_id":

json_web_token.rbの修正が必要

class JsonWebToken
  SECRET_KEY = Rails.application.credentials.secret_key_base || Rails.application.secret_key_base

  def self.encode(payload, exp = 24.hours.from_now)
    payload[:exp] = exp.to_i
    JWT.encode(payload, SECRET_KEY)
  end

  def self.decode(token)
    decoded = JWT.decode(token, SECRET_KEY)[0]
    HashWithIndifferentAccess.new decoded
  rescue JWT::DecodeError
    nil
  end
end

新規登録をやり直す

curl -X POST http://localhost:3000/signup \
     -H "Content-Type: application/json" \
     -d '{"email": "hoge@example.com", "password": "password123", "password_confirmation": "password123"}'

多分成功?

hashimotojunichi@hashimotojunichinoMacBook-Pro icchy_api % curl -X POST http://localhost:3000/signup \
     -H "Content-Type: application/json" \
     -d '{"email": "hoge@example.com", "password": "password123", "password_confirmation": "password123"}'
{"token":"eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJleHAiOjE3MjY0ODczMzh9.cZIw6yQCFd-yztVfQavxbqC6yk_zTex2Lt-8MXG_6bI","exp":"09-16-2024 20:48","email":"hoge@example.com"}%                                                                                                                                                      hashimotojunichi@hashimotojunichinoMacBook-Pro icchy_api % 

curlでログインしてみる

curl -X POST http://localhost:3000/auth/login \
     -H "Content-Type: application/json" \
     -d '{"email": "hoge@example.com", "password": "password123"}'

成功したみたいだな。

hashimotojunichi@hashimotojunichinoMacBook-Pro icchy_api % curl -X POST http://localhost:3000/auth/login \
     -H "Content-Type: application/json" \
     -d '{"email": "hoge@example.com", "password": "password123"}'
{"token":"eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJleHAiOjE3MjY0ODc0Njl9.rR285wCTwifW0KGiIAjoxaIBdjQOjhF_DPdzMGClKeg","exp":"09-16-2024 20:51","email":"hoge@example.com"}%                                                                                                                                                      hashimotojunichi@hashimotojunichinoMacBook-Pro icchy_api %

感想

まだサンプル作って練習しているところなのでゆっくり時間をかけて勉強したいなと思います。サーバーを構築してデータベースも構築して認証機能があるREST APIを作るのが目標ですね。

Discussion