今回達成すること
ログインコントローラーの役目を持つ、
ログインコントローラーの責務
ログインコントローラーは、Nuxt.jsからきたログイン情報を受け取り
ユーザーを検索します。
ユーザーが存在した場合はトークンを発行し、Cookieに保存します。
Nuxt.jsには有効期限のみを返します。
今回はここまでの実装を行います。
user_tokenコントローラーを作成する
ログイン認証を行う
root $ docker-compose run --rm api rails g controller Api::V1::UserToken
privateメソッド群を作成する
ログイン実行アクションの下準備で、private
メソッドを作成します。
private
メソッドでは、Nuxt.jsからのパラメーター受け取りと、ユーザーの検索を行います。
メールアドレスからアクティブなユーザーを探す
Nuxt.jsから渡されたメールアドレスからアクティブなユーザーを検索する、entity
メソッドを追加します。
api/app/controllers/api/v1/user_token_controller.rb(以下、省略)
class Api::V1::UserTokenController < ApplicationController
# 以下、追加
private
# メールアドレスからアクティブなユーザーを返す
def entity
@_entity ||= User.find_activated(auth_params[:email])
end
def auth_params
params.require(:auth).permit(:email, :password)
end
end
トークンを発行する
返されたユーザーIDを含むトークンを発行する、auth
メソッドを追加します。
class Api::V1::UserTokenController < ApplicationController
private
# 追加
# トークンを発行する
def auth
@_auth ||= UserAuth::AuthToken.new(payload: { sub: entity.id })
end
...
end
Cookieに保存するトークンを設定する
発行したトークンをCookieに保存するための設定を行います。
class Api::V1::UserTokenController < ApplicationController
private
# 追加
# クッキーに保存するトークン
def cookie_token
{
value: auth.token,
expires: Time.at(auth.payload[:exp]),
secure: Rails.env.production?,
http_only: true
}
end
...
end
-
value
... Cookieの値。 -
expires(エクスパイアーズ)
... Cookieの有効期限。ここの値を設定しないと、ブラウザを閉じた際にCookieは削除されます。
-
secure(セキュア)
...https
通信でしかアクセスできないCookieとなる。本番環境のみ有効にしています。
-
http_only
... JavaScriptからアクセスできないCookieとなる。
パスワード一致を検証する
ログインアクション前に実行するパスワード検証メソッド、authenticate
を追加します。
class Api::V1::UserTokenController < ApplicationController
private
# 追加
# entityが存在しない、entityのパスワードが一致しない場合に404エラーを返す
def authenticate
unless entity.present? && entity.authenticate(auth_params[:password])
raise UserAuth.not_found_exception_class
end
end
...
このメソッドは、
- メールアドレスに一致するユーザーが居ない
- パスワード一致しない
場合に、RecordNotFound
エラーを吐きます。
404のヘッダーレスポンスを返す
Rails側でRecordNotFound
エラーが発生した場合、Nuxt.jsは、「ユーザーが存在しません」などのトースターを表示するだけで、Railsが吐き出すエラー画面(HTML)は必要ありません。
RecordNotFoundをそのまま返した時のレスポンス
そこでRecordNotFound
エラーを、ヘッダーレスポンスのみを返すメソッドに集約します。
class Api::V1::UserTokenController < ApplicationController
# 追加
rescue_from UserAuth.not_found_exception_class, with: :not_found
private
# 追加
# NotFoundエラー発生時にヘッダーレスポンスのみを返す
# status => Rack::Utils::SYMBOL_TO_STATUS_CODE
def not_found
head(:not_found)
end
...
-
rescue_from エラークラス, with: :メソッド名
... 引数のエラーが発生した際に、処理をメソッドに委任する。 -
head()
... 本文 (body) のない、ヘッダーのみのレスポンスを返す。
head(:not_found)にエラー処理を渡した時のレスポンス
これで下準備ができました。
ログインアクションを作成する
ログイン処理は、create
アクションで行います。
create
アクションでは、
- Cookieをリセットする
- ユーザーを取得する
- トークンをCookieに保存する
- トークンのユーザーを返す
この4つを実行します。
api/app/controllers/api/v1/user_token_controller.rb
class Api::V1::UserTokenController < ApplicationController
rescue_from UserAuth.not_found_exception_class, with: :not_found
# 以下、追加
before_action :delete_cookie
before_action :authenticate, only: [:create]
# login
def create
cookies[token_access_key] = cookie_token
render json: {
exp: auth.payload[:exp],
user: entity.my_json
}
end
private
...
delete_cookie
...Authenticator
モジュールのdelete_cookie
メソッドが実行され、Cookieが削除されます。authenticate
...private
メソッドが実行され、ユーザーを検索します。cookies[token_access_key]
... トークンをCookieに保存します。render json: {}
... 有効期限と、ユーザーをNuxt.jsに返します。
ログアウトアクションを作成する
ログアウトは、destroy
アクションで行います。
destroy
アクションでRailsが行うことは、Cookieを削除するだけです。
before_action :delete_cookie
で削除されるので、リクエストが正しく通った時のレスポンスだけ返せばOKです。
api/app/controllers/api/v1/user_token_controller.rb
...
# login
def create
cookies[token_access_key] = cookie_token
render json: {
exp: auth.payload[:exp],
user: entity.my_json
}
end
# 追加
# logout
def destroy
head(:ok)
end
private
...
ルートを追加する
最後はcreate
とdestroy
アクションへアクセスするルートを
api/config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
# users
resources :users, only:[] do
get :current_user, action: :show, on: :collection
end
# 追加
# login, logout
resources :user_token, only: [:create] do
delete :destroy, on: :collection
end
# ここまで
end
end
end
ログインコントローラーの実装は以上です。
コミットしとく
ここまでの変更をコミットしましょう。
root $ cd api
api $ git add -A && git commit -m "add_user_token_controller.rb" && cd ..
root $
まとめ
今回は、ログインアクションを実行する
サーバーサイドではCookieに保存したトークンを検証して、ログインを判定します。
クライアントではサーバーから返された有効期限をログイン判定に使用します。
これでログイン認証は完璧!ではありません。
今回実装した方法は、最低限のセキュリティを考慮したものであって、セキュリティを高ようと思えばもっと改善が必要です。(筆者はそこまでの知識を持ち合わせていない:face_with_head_bandage: )
ただ、「セキュリティが高ければ高いほど良い」というものでもなく、そこには時間とお金がかかるので、アプリに適したセキュリティ対応を行う必要があります。
まずは「どんなアプリを作るか」を明確にして、「そのアプリのセキュリティはどの程度必要か」を考え、時間とお金を投資しましょう。
(そもそも、自分で実装せずAuth0のような外部サービスに任せるといった選択肢もありますので。)
次回予告
今回作成した
次へ進みましょう。(↓)