【rails】authenticate_with_http_token を使ってapiの認証機能を実装する

はじめに

今回やりたかったことはアプリからのリクエストを受け付けるapiの開発。やりたいことと流れは以下のようなイメージ

・Emailとパスワードでログインを行う
・ログインに成功したらトークンと有効期限を発行してクライアントに返す
・クライアントは毎リクエストークンをhttpヘッダーに入れてリクエストする

また、今回はrails5のapiモードを使用しました。

テーブル設計とか準備

・users

idユーザーid
emailメール
password_digestパスワード
tokenトーク
token_expireトークン有効期限

password_digestはrailsのhas_secure_passwordというのを使う場合にこのカラムが必要なようなので追加。has_secure_passwordはパスワード認証の仕組みを簡単に実装できる仕組みみたい。

とりあえずモデルとテーブルを作成する

$ ./bin/rails g model User

migrationファイルが生成されるのでカラムを追記する
db/migrate/20171009182615_create_users.rb

class CreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|

      t.string :email
      t.string :password_digest
      t.string :token
      t.datetime :token_expire

      t.timestamps
    end
  end
end

modelファイルも生成されるのでバリデーションを追加
app/models/user.rb

class User < ApplicationRecord

  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }

  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

end

has_secure_passwordを使うためにbcryptというのが必要なのでGemfileに追記
Gemfile

gem 'bcrypt', '~> 3.1.7'

ここまでできたら以下を実行

$ ./bin/bundle install
$ ./bin/rails db:migrate

実装

あとはプログラムを実装していく。

config/routes.rb

Rails.application.routes.draw do
  post '/users/signup', to:'users#signup'
  post '/users/signin', to:'users#signin'
end

app/controller/users_controller.rb

# coding: utf-8                                                                                                                                                                                                    
class UsersController < ApplicationController

  ## authenticate_with_http_tokenを使うために
  include ActionController::HttpAuthentication::Token::ControllerMethods

  ## signinとsingup以外で認証処理を行う
  before_action :auth, except: [:signin, :signup]

  ## サインアップ
  def signup
    user = User.new(user_params)
    if user.save
      success
    else
      failed(user.errors.full_messages)
    end
  end

  ## サインイン
  def signin                                                                                                                                                                                                       
    ## 認証に成功したらランダム文字列でトークンを生成して有効期限1日をセット
    user = User.find_by(email: params[:email])
    if user && user.authenticate(params[:password])
      user.token = create_token
      user.token_expire = DateTime.now + 1
      if user.save!(:validate => false)
        success({ token: user.token })
      else
        error
      end
    else
      error('email または passwordが正しくありません')
    end
  end                                                                                                                                                                                                              

  ## レスポンス返却用の簡易メソッド                                                                                                                                                                                
  def success(data = nil)
    render json:{ status: 'success', result: data }
  end

  def error(messages = nil)
    render json:{ status: 'failed', messages: messages }
  end

  ## 以下 authenticate_with_http_token を使った処理 
  def auth                                                                                                                                                                                                         
    ## 認証に失敗したらエラーレスポンスを返す
    authenticate_token || render_unauthorized
  end

  ## httpヘッダーに入っているtokenでdbを検索して有効期限のチェック
  ## authenticate_with_http_tokenの書き方でこのように書くみたい
  def authenticate_token
    authenticate_with_http_token do |token, options|
      user = User.find_by(token: token)
      user && DateTime.now <= user.token_expire
    end
  end

  def render_unauthorized
    render json:{ result:'failed' }
  end

  ## ランダム文字列を作る
  def create_token
    return ((0..9).to_a + ("a".."z").to_a + ("A".."Z").to_a).sample(50).join
  end

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

end


確認はこんな感じで

$ curl -H 'Authorization: Token xxxxxxxxxxxxxxxxxxxxxxxxxx' http://localhost:3000/users


今回はトークンをDBに入れているけど、少し規模の大きいサービスだったらredisとかにもたせるのがよいのかな。
以上です。