トークンを使ってAPIの認証機能をつくる【rails】
はじめに
構成
ApiKeyモデルを作成
まずはApiKeyモデルを作成します。
$ rails g model ApiKey user:references access_token:string expires_at:datetime
class CreateApiKeys < ActiveRecord::Migration[6.0] def change create_table :api_keys do |t| t.references :user, null: false, foreign_key: true t.string :access_token, null: false t.datetime :expires_at t.timestamps t.index :access_token, unique: true end end end
access_token
カラムにNOT NULL制約とインデックスを付与します。
$ rails db:migrate
モデルの関連付け
userモデルに関連付けを指定します。
has_many :api_keys, dependent: :destroy
initializeメソッドを上書き
ApiKeyの初期化時に期限つきのトークンを作成したいので、initializeメソッドを上書きします。
また、有効なトークンだけをとってくるscopeを追加します。
class ApiKey < ApplicationRecord belongs_to :user validates :access_token, presence: true, uniqueness: true scope :valid_expire, -> { where('expires_at > ?', Time.current) } def initialize(attributes = {}) super self.access_token = SecureRandom.urlsafe_base64 self.expires_at = 1.week.from_now end end
トークンに関するメソッドを作成
続いて、userモデルに有効なトークンを返すメソッドを追記します。
def grant_api_key return api_keys.valid_expire.first if api_keys.valid_expire.exists? # 有効なトークンがあればその中で一番古いトークンを返す。 # なければ新しいトークンを作成。 api_keys.create end
次は継承元のbase_controller
に、レスポンスヘッダーにトークンを入れる処理を書きます。
def set_token(user) api_key = user.grant_api_key response.headers['AccessToken'] = api_key.access_token end
ユーザー登録時とログイン時にトークンをセットするために、先程定義したset_token
メソッドを呼び出します。
module Api module V1 class RegistrationsController < BaseController def create user = User.new(user_params) if user.save json_string = UserSerializer.new(user).serialized_json set_token(user) # 追加 render json: json_string else render_400(nil, user.errors.full_messages) end end private def user_params params.require(:user).permit(:name, :email, :password) end end end end
module Api module V1 class AuthenticationsController < BaseController def create user = login(params[:email], params[:password]) raise ActiveRecord::RecordNotFound unless user json_string = UserSerializer.new(user).serialized_json set_token(user) # 追加 render json: json_string end private def form_authenticity_token; end end end end
これでユーザー新規作成時とログイン時に、有効なトークンがなければ発行し、レスポンスヘッダーに入れます。
そして有効なトークンがあれば、それをレスポンスヘッダーにいれる処理が完了しました。
ページアクセスする際の認証
記事一覧ページなどにアクセスする際に、先程のトークンを認証する処理をつくります。
bese_controller
にauthenticateメソッドを追加します。
module Api module V1 class BaseController < ApplicationController include Api::ExceptionHandler include ActionController::HttpAuthentication::Token::ControllerMethods # authenticate_or_request_with_http_tokenメソッドを使うために必要。 private def set_token(user) api_key = user.grant_api_key response.headers['AccessToken'] = api_key.access_token end def authenticate authenticate_or_request_with_http_token do |token, _options| @_current_user ||= ApiKey.valid_expire.find_by(access_token: token)&.user end end def current_user @_current_user # `_` は直接参照してほしくない変数につける。 end end end end
authenticate_or_request_with_http_tokenHTTP
メソッドでトークンの認証をおこないます。
falseの場合はToken: Access denied.
を返します。
current_user
を使えるようにするためにcurrent_user
メソッドも定義しています。
最後に、articles_controller
に先程定義したauthenticate
メソッドを、before_actionに設定します。
module Api module V1 class ArticlesController < BaseController before_action :authenticate # 追加 def index articles = Article.all json_string = ArticleSerializer.new(articles).serialized_json render json: json_string end def show article = Article.find(params[:id]) options = { include: %i[user 'user.name' 'user.email'] } json_string = ArticleSerializer.new(article, options).serialized_json render json: json_string end end end end