大草原

技術ブログ

Rails5 APIモードでつくるかんたんなトークンベース認証

はじめに

Ruby on RailsのAPIモードで、モバイルアプリケーションやSPAのためのかんたんなトークンベース認証を実装する例。

なお、タイトルの通りかんたんなものを目指しているので、複雑なことをしたいなら、 devise_token_auth等を使ったほうが早い場合もある。

検証環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.1
BuildVersion:   18B75

$ ruby -v
ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-darwin18]

$ rails -v
Rails 5.2.1

プロジェクトの作成

$ rails new sample-api --api --database postgresql -T
  • --api でAPIモードを指定。
  • --database はお好きなもの。この記事では、Herokuにデプロイすることも考えてPostgreSQLを指定する。
  • -T で テスト関連のファイルの生成をスキップする。

初期設定

DBの作成とマイグレーション

$ rails db:create db:migrate

Rack::CORSまわりの初期設定

Gemfile の29行目あたりのコメントアウトを外す。

# Gemfile
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors'

bundle install 実行

$ bundle install

config/initializers/cors.rb を編集。コメントアウトされた部分の # を外し、 origins* を指定する。

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

CORS(Cross-Origin Resource Sharing)についてはこの辺を参照。

オリジン間リソース共有 (CORS) - HTTP | MDN

要するに、クロスドメイン制約というセキュリティ上の理由でサーバと通信できるドメインや許可するHTTPヘッダやメソッドを絞る機構があるのだけど、APIサーバでそれをやられると困るので、全部許可しておくという設定をしたということだ。

Userモデル

bcrypt gemのインストール

前準備として、 has_secure_password を利用するために bcrypt gemをインストールする。Gemfile の17行目あたりにある bcrypt のコメントアウトを外して、 bundle install を実行する。

# Gemfile
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'
$ bundle install

Userモデルの作成

Userモデルを作成する。

$ rails g model User email:string name:string password_digest:string token:string

生成された db/migrate/20181114012824_create_users.rb を編集する。

  • token以外の各カラムにNOT NULL制約を追加
    • tokenカラムにはNOT NULL制約を追加しない。*1
  • email カラムにUNIQUE制約を追加
  • token カラムにUNIQUE制約を追加
# db/migrate/20181114012824_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :email, null: false
      t.string :name, null: false
      t.string :password_digest, null: false
      t.string :token

      t.timestamps
    end
    add_index :users, :email, unique: true
    add_index :users, :token, unique: true
  end
end

app/models/user.rb を編集。 has_secure_passwordhas_secure_token の追記、その他バリデーション。

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password
  has_secure_token

  validates :email, presence: true
  validates :email, uniqueness: true
  validates :name, presence: true
  validates :password_digest, presence: true
  validates :token, uniqueness: true
end

DBのマイグレーションを行う。

$ rails db:migrate

rails consoleで動作確認をする。

user = User.new(name: 'mktakuya', email: 'mktakuya@example.com', password: 'password')
user.save
#=> true

user.token
#=> "何らかの文字列"

user.authenticate('password')
#=> #<User id: 1, name: "mktakuya", ... >

user.authenticate('wrong_password')
#=> false

認証処理の実装

ヘルパーメソッドの追加

ApplicationController に認証用のヘルパーメソッドを追加する。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include ActionController::HttpAuthentication::Token::ControllerMethods

  before_action :authenticate!

  private

  def authenticate!
    authenticate_or_request_with_http_token do |token, options|
      User.find_by(token: token).present?
    end
  end

  def current_user
    @current_user ||= User.find_by(token: request.headers['Authorization'].split[1])
  end
end
  • before_action :authenticate! により、全Controllerの全Actionに認証をかける事ができる。
    • 認証無しでもアクセスできるようにしたい場合は、後述する skip_before_action を利用する。
  • Controller内で current_user メソッドを呼び出すと、ログイン中のユーザの情報にアクセスすることが出来る。

UsersControllerの実装

UsersController生成

UsersController を作成する。

$ rails g controller Users create sign_in

config/routes.rb を編集

# config/routes.rb
Rails.application.routes.draw do
  resources :users, only: [ :create ] do
    collection do
      post 'sign_in'
    end
  end
end

ログイン

ログイン処理は、 users#sign_inUsersControllersign_in アクションを指す) に記述する。

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  skip_before_action :authenticate!, only: [ :create, :sign_in ]

  def create
  end

  def sign_in
    @user = User.find_by(email: params[:email])

    if @user && @user.authenticate(params[:password])
      render json: @user
    else
      render json: { errors: ['ログインに失敗しました'] }, status: 401
    end
  end
end
  • ログイン処理の大まかな流れは、Rails Tutorialで作成した Sessions#create と同じ*2。ただし、処理終了後にどこかにリダイレクトしたりviewをレンダリングしたりするのではなく、UserのJSONを返すか、エラーメッセージを返すかである。
  • skip_before_actionauthenticate! を指定するのを忘れないこと。
    • ログインや新規作成の前にログイン状態で無いのは当たり前である。

新規登録

新規登録処理は、 Users#create に記述する。

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  skip_before_action :authenticate!, only: [ :create, :sign_in ]

  def create
    @user = User.new(email: params[:email], password: params[:password], name: params[:name])

    if @user.save
      render json: @user
    else
      render json: { errors: @user.errors.full_messages }, status: 400
    end
  end

  def sign_in
  ### 省略 ###
  end
end

動作確認

ログイン中ユーザの情報を返すアクションを作成

動作確認用に、ログイン中ユーザの情報を返すアクション users#meUsersController に作成する。

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  skip_before_action :authenticate!, only: [ :create, :sign_in ]

  ### 省略 ###

  def me
    render json: current_user
  end
end

config/routes.rb を編集する。

Rails.application.routes.draw do
  resources :users, only: [ :create ] do
    collection do
      post 'sign_in'
      get 'me' # ← 追加
    end
  end
end

Insomniaのインストール

普通のWebサイトならWebブラウザで動作確認すればいいのだが、これはAPIサーバなので、何らかのHTTPクライアントを使う必要がある。curlコマンドで頑張ってもいいけど、せっかくなのでInsomniaというイケてるソフトウェアを使いましょう。

Insomnia REST Client

新規登録

新規リクエストを以下の要領で作成する。

  • Name: /users/
  • Method: POST
  • Body: JSON

画面上部のURL欄に http://localhost:3000/users/ をセットし、Body欄に以下のJSONを記述する。

{
  "email": "user@example.com",
  "password": "password",
  "name": "Example太郎"
}

スクショのとおりになっていればOK。

f:id:mktakuyax:20181114155431p:plain

Sendボタンを押してリクエストを送信し、ユーザ登録が完了するとユーザ情報が降ってくる。

f:id:mktakuyax:20181114160314p:plain

token はともかく、 password_digest も一緒に降ってくるのはどうなんだという話もあるが、それはまた別の話なので、この記事では省略する。

ログイン

同じように、ログイン処理の新規リクエストを作成する。

  • Name: /users/sign_in
  • Medhod: POST
  • Body: JSON

URL欄は http://localhost:3000/users/sign_in 、Body欄にはログイン情報をJSONで記述。

{
  "email": "user@example.com",
  "password": "password"
}

Sendボタンを押してリクエストを送信し、ログインに成功するとユーザ情報が降ってくる。今後、このレスポンスに含まれるtokenを利用してサーバとの通信を行う。 f:id:mktakuyax:20181114161358p:plain

/users/me の動作確認

tokenを利用したサーバとの通信の例として、ログイン中ユーザの情報を取得する /users/me にリクエストを送信してみる。

新規リクエストを以下の要領で作成する。

  • Name: /users/me
  • Method: GET

tokenはHTTPのヘッダに含めるので、AuthタブからBearerを選択し、tokenを設定する。正しいtokenが設定されていると、ログイン中ユーザの情報がレスポンスとして返ってくる。

f:id:mktakuyax:20181114161027g:plain

おわりに

こんな感じで、かんたんなトークンベース認証の機構を作ることが出来た。

Insomniaでの検証時はrails consoleからトークンをコピペしてセットしていたが、実際にモバイルアプリケーションを作るときは、Emailとパスワードによるログイン後、レスポンスとして返ってきたトークンをiOSのUserDefaultsに保存し以後のリクエスト送信で利用するなどすれば良い。

なお、今回作ったトークンベース認証の機構は、Railsのデフォルトの機能だけを使って実現しているので、非常にシンプルなものとなっている。もっと複雑なこと、例えばログイン元ごとに複数のトークンを管理したいだとか、トークンに有効期限を設けたいとかなったら、 devise_token_auth gemを使ったりすると良いと思う。