フルスタックエンジニアに

フルスタックエンジニアを目指すブログです。主にRuby on RailsやJavaScriptについて書いていきたいと思います。

JavaScriptフレームワーク比較用サンプルアプリ 「TODO-Module」を作る (3/3)

TODO-Module-Rails-eye-catch

前回までで作成したRailsアプリケーションのAPIを作っていきます。

www.full-stack-engineer.com

ユーザーのモデルに認証トークンを追加する

Webアプリケーションの場合は、セッションを利用してログイン状態を保持していますが、 APIの場合はそれができません。
何らかの方法でリクエストごとに認証してユーザーを特定する必要があるのですが、 これにメールアドレスとパスワードを使ってしまうのはセキュリティ上よくありません。
これらに代わる認証トークンを作成し、それで認証を行いようにします。 認証トークンを保存するカラムをusersテーブルに追加し、 ユーザー作成時に自動的に認証トークンも生成されるようにしましょう。

% bin/rails g migration AddAuthenticationTokenToUsers authentication_token:string
      invoke  active_record
      create    db/migrate/20150322132050_add_authentication_token_to_users.rb

% bin/rake db:migrate
== 20150322132050 AddAuthenticationTokenToUsers: migrating ====================
-- add_column(:users, :authentication_token, :string)
   -> 0.1604s
== 20150322132050 AddAuthenticationTokenToUsers: migrated (0.1605s) ===========

/app/models/user.rb

class User < ActiveRecord::Base

  authenticates_with_sorcery!

  has_many :tasks

  validates :email, presence: true, uniqueness: true, email_format: {message: "無効なメールアドレスです"}
  validates :password, presence: true, length: {minimum: 8}, confirmation: true, if: :password_required?
  validates :password_confirmation, presence: true, if: :password_required?

  before_create do |user|
    user.reset_authentication_token
  end

  def reset_authentication_token
    self.authentication_token = generate_authentication_token
  end

  def reset_authentication_token!
    reset_authentication_token
    save!
  end
  
  private
  
  def generate_authentication_token
    loop do
      token = SecureRandom.uuid
      break token unless User.where(authentication_token: token).first
    end
  end

  def password_required?
    new_record? || password.present? || password_confirmation.present?
  end
  
end

versionistでAPIのひな形を作る

APIはversionistというgemを使って作っていきます。 同じコントローラーにHTMLを返す処理とJSONを返す処理を書くこともできるのですが、 それをしてしまうと見通しが悪くなってしまうので、API用のコントローラーは分けたいと思います。

/Gemfile

gem 'versionist', '1.4.0'
% bundle install
% bin/rails g versionist:new_api_version v1 V1 --path=value:v1
      route  api_version(:module => "V1", :path => {:value => "v1"}) do end
      create  app/controllers/v1
      create  app/controllers/v1/base_controller.rb
      create  spec/controllers/v1
      create  spec/controllers/v1/base_controller_spec.rb
      create  spec/requests/v1
      create  spec/requests/v1/base_controller_spec.rb
      create  app/presenters/v1
      create  app/presenters/v1/base_presenter.rb
      create  spec/presenters/v1
      create  spec/presenters/v1/base_presenter_spec.rb
      create  app/helpers/v1
      create  spec/helpers/v1
      create  public/docs/v1
      create  public/docs/v1/index.html
      create  public/docs/v1/style.css

app/controllers/v1/users_controller.rb

class V1::UsersController < V1::BaseController

  skip_before_filter :require_login, only: [:create]

  def create
    @user = User.new(user_params)
    if @user.save
      render status: 201, json: @user.to_json(only: [:id, :email, :authentication_token])
    else
      render :json => @user.errors.full_messages, status: :unprocessable_entity
    end
  end

  private

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

では動作確認をしてみましょう。 APIの動作確認にはPostmanが便利なのでこれを使っていきます。
http://localhost:3000/v1/usersにパラメーター無しでPOSTリクエストを送ってみましょう。
本来ならばバリデーションエラーのメッセージが帰ってくるはずですが、 ActionController::InvalidAuthenticityTokenというエラーが発生してしまいました。

TODO-Module API GET :users 1

これはCSRF対策がデフォルトで有効になっているためなので、それを無効にします。

/app/controllers/application_controller.rb

-protect_from_forgery with: :exception
+protect_from_forgery with: :null_session

では改めてリクエストを送ってみましょう。 ActionController::InvalidAuthenticityTokenが発生しなくなりましたね。
代わりにActionController::ParameterMissingが発生しましたが、 これはパラメーターを送っていないためなので問題ありません。

TODO-Module API GET :users 2

正しいパラメーターを送ってユーザーが作成されることを確認しましょう。

パラメーター 内容
user[email] メールアドレス
user[password] パスワード
user[password_confirmation] パスワード(確認)

TODO-Module API POST :sessions

ちゃんとトークンも生成されているようですね。
クライアント側でこのトークンを保存して、リクエストを投げるときはこのトークンも一緒に投げるようにします。

セッションコントローラーを作る

rails g versionist:new_controller Sessions V1
      create  app/controllers/v1/sessions_controller.rb
      create  spec/controllers/v1/sessions_controller_spec.rb
      create  spec/requests/v1/sessions_controller_spec.rb

app/controllers/v1/sessions_controller.rb

class V1::SessionsController < V1::BaseController

  skip_before_filter :require_login

  def create
    if @user = login(params[:email], params[:password])
      render status: 201, json: @user.to_json(only: [:id, :email, :authentication_token])
    else
      head(401)
    end
  end

end

これはログインに成功したら201ユーザー情報を返して、失敗したら401を返すだけなので非常に簡単です。
動作確認をしてみましょう。 http://localhost:3000/v1/sessionsにPOSTリクエストを送ってください。

パラメーター 内容
email メールアドレス
password パスワード

TODO-Module API POST :sessions

タスクコントローラーを作る

rails g versionist:new_controller Tasks V1
      create  app/controllers/v1/tasks_controller.rb
      create  spec/controllers/v1/tasks_controller_spec.rb
      create  spec/requests/v1/tasks_controller_spec.rb

app/controllers/v1/tasks_controller.rb

class V1::TasksController < V1::BaseController

  before_action :set_task, only: [:show, :update, :destroy, :complete, :revert]

  def index
    render json: current_user.tasks
  end

  def create
    @task = current_user.tasks.build(task_params)
    if @task.save
      render json: @task, status: 201
    else
      render json: @task.errors.full_messages, status: 422
    end
  end

  def show
    render json: @task
  end

  def update
    if @task.update(task_params)
      render json: @task
    else
      render json: @task.errors.full_messages, status: 422
    end
  end

  def complete
    @task.complete!
    render json: @task
  end

  def revert
    @task.revert!
    render json: @task
  end

  def destroy
    @task.delete!
    render json: @task
  end
  
  private

  def task_params
    params.require(:task).permit(:title, :memo)
  end

  def set_task
    @task = current_user.tasks.find(params[:id])
  end
end

/config/routes.rb

Rails.application.routes.draw do
・
・
・
    api_version(:module => "V1", :path => {:value => "v1"}, :defaults => {:format => "json"}) do

        resources :users, only: [:create]
        resources :sessions, only: [:create]

        resources :tasks, only: [:index, :create, :show, :update, :destroy] do
            member do
                match "complete", via: [:patch, :put]
                match "revert", via: [:patch, :put]
            end
        end
    end
end

処理自体は通常のタスクコントローラーを一緒です。 テンプレートをレンダリングする代わりにJSONデータを返したり、 set_countメソッドを削ったりしています。タスクの数はクライアント側で計算すればいいですからね。

トークンで認証する機能を作る

ユーザー作成時にトークンを生成する機能は作りましたが、これを使って認証する機能は作っていませんでしたね。 これを作ります。 application_controller.rbを以下のように変更します。

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :null_session

  before_filter :authenticate_user_from_token!
  before_filter :require_login

  private

  def authenticate_user_from_token!
    if token = request.headers["Authorization"]
      if @user = User.where(authentication_token: token).first
        auto_login(@user)
      end
    end
  end
  
  def not_authenticated
    if request[:format] == "json"
      head(401)
    else
      redirect_to root_path, alert: "ログインしてください。"
    end
  end
  
end

Authorizationヘッダにトークンが含まれていた場合はそれで認証をし、 それがなければ今までどおりセッションで認証をします。

これでAPIの作成は完了です!動作確認をしてみましょう!
http://localhost:3000/v1/tasksPOSTリクエストを送ってみます。 Authorizationヘッダを送るのも忘れないようにしましょう。

パラメーター 内容
task[title] タイトル
task[memo] メモ

TODO-Module API POST :tasks

ちゃんと作成されているようですね。ではタスク取得もしてみましょう。
http://localhost:3000/v1/tasksにGETリクエストを送ります。

TODO-Module API GET :tasks

先ほど作ったタスクが返ってきました! 他にもタスクを完了にしたり変更したりと確認をしてみてください。

クライアントを作る準備

次回からこのAPIを使ったクライアントを作っていきますが、このままだとクロスドメインで弾かれてしまいます。 なのでその対策をしておきます。
対策と言ってもrack-corsというgemを使うだけです。

/Gemfile

gem 'rack-cors', '0.3.1', :require => 'rack/cors'
% bundle install

/config/appliction.rb

module TodoModule
  class Application < Rails::Application
  ・
  ・
  ・
    config.middleware.insert_before 0, "Rack::Cors" do
      allow do
        origins '*'
        resource '*', :headers => :any, :methods => [:get, :post, :patch, :put, :delete, :options]
      end
    end
  end
end

これで別ドメインからアクセスされても正しくレスポンスを返すことができます。
次回から様々なJavaScriptフレームワークを使ってSPA(シングルページアプリケーション)のクライアントを作っていきたいと思います。

www.full-stack-engineer.com