今回はNuxt.jsとrails APIモードを使ったJWT認証(JSON Web Tokens)の実装についてです。JWT実装はサーバーサイド(rails)とクライアントサイド(Nuxt.jsの)二つのパートに分けて書いていきます。今回はサーバーサイドの実装を確認していきましょう!
JWT認証の流れ
JWT認証の流れは以下の図のようになります。
- ユーザーがメールアドレスとパスワードを入力
- サーバーサイド側でメールアドレスが存在するかDBを検索、signupの場合新しいレコードを作成。
- JWTトークンを生成しクライアント側に渡す。
- ユーザーが認証が必要なリソースが必要な時トークンを検証
Jwt認証に必要なGemファイルをインストール
今回、bcryptとjwtのgemファイルを用意します。bcryptはDBに保存するパスワードを暗号化するためのファイル。jwtはjwt認証に必要なファイルになります。
gem 'bcrypt', '~> 3.1.7'
gem 'jwt'
Gemファイルに記述したらrails bundle installをコマンドで実行します。
UserとAuthコントローラーを作成
続いて、UserとAuthコントローラーを作成します。コマンドで以下を実行します。Userコントローラーはユーザーのサインアップ用に使います。
rails g controller Users create
rails g controller Auth login auto_login
routesの確認
今回、APIとして使用するので、名前空間を作成しておくことで、今後のAPIのバージョン管理が用意になります。resource :users, only: [:create]はサインアップ用で今回はcreateのみ使用します。post “/login”, to: “auth#login”はログイン用です。get “/auto_login”, to: “auth#auto_login”はユーザーが保持しているトークンが有効か確認する時に必要になります。
Rails.application.routes.draw do
namespace 'api' do
namespace 'v1' do
resource :users, only: [:create]
post "/login", to: "auth#login"
get "/auto_login", to: "auth#auto_login"
end
end
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
サインアップ用のUserコントローラー
サインアップ用のコードをUserコントローラーに記述していきます。ユーザーがサインアップすると新しいユーザーインスタンスが作成されます。クライアントサイドからPOSTリクエストが行われ、ユーザーインスタンスが有効だった場合、ユーザーidを使用してトークンを生成してクライアント側に渡します。
User Controller
module Api
module V1
class UsersController < ApplicationController
skip_before_action :require_login, only: [:create]
def create
user = User.create(user_params)
if user.valid?
payload = {user_id: user.id}
token = encode_token(payload)
render json: {user: user.user_id, jwt: token}
else
render json: {errors: user.errors.full_messages},status: :not_acceptable
end
end
private
def user_params
params.permit(:email,:password)
end
end
end
end
Application Controllerで401エラーを定義
401エラーは何度か使うので、Application Controllerで定義して共通化してあげます。
Application Controller
# 401 Unauthorized
def response_unauthorized
render status: 401, json: { status: 401, message: 'Unauthorized' }
end
続いてAuthコントローラーです。まずloginメソッドを作成していきます。クライアント側から受け取ったメールアドレスと一致するものがあるかfind_byを使ってDBのレコードを検索します。一致するメールアドレスが存在する場合、authenticateメソッドを使ってパスワードが一致するか確認します。パスワードが有効だった場合、encode_tokenメソッドを使ってトークンを生成し、クライアントに渡します。
Auth Controller
module Api
module V1
class AuthController < ApplicationController
skip_before_action :require_login, only: [:login, :auto_login]
def login
user = User.find_by(email: params[:email])
if user && user.authenticate(params[:password])
payload = {user_id: user.id,email: user.email}
token = encode_token(payload)
render json: {jwt: token,success: "Welcome back, #{user.email}"}
else
response_unauthorized
end
end
end
end
end
JWTトークンに必要なメソッドを用意
encode_tokenメソッドはApplication Controllerで定義してあげます。encodeメソッドは最初にインストールしたgemファイル’jwt’に含まれており、JWT認証に必要なtokenを生成します。第一引数にはユーザーidが含まれているpayload、第二引数には秘密鍵。第三引数はアルゴリズムを指定してあげます。
Application Controller
def encode_token(payload)
JWT.encode(payload,'my_secret_key','HS256')
end
そうすると以下のようなtokenが生成されます。
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.RbmQ6qKdj8y6GeTBX0EGbLst3wrHuGrNGGOuT5pRPls"
JWTトークンの検証
続いて、ログインしたユーザーが保持しているトークンが適切かどうか検証するコードを記述していきます。Authcontrollerにauto_loginメソッドを追加します。クライアントから送られてきたトークンを検証していきます。
Auth Controller
def auto_login
if session_user
render json: session_user
else
render json: {errors: "No user Logged In"}
end
end
applicationコントローラーにsession_userメソッドを定義してあげます。decoded_tokenの値が存在する場合、user_idを取り出してDBで検索します。一致するuser_idが存在していたら、auto_loginメソッドに返してあげてクライアント側にデータを渡します。
def session_user
decoded_hash = decoded_token
if !decoded_hash.empty?
user_id = decoded_hash[0]['user_id']
@user = User.find_by(id: user_id)
else
nil
end
end
session_userメソッドでトークンを検証するためにapplication controllerにdecoded_tokenとauth_headerを定義します。トークンはheaderのAuthorizationにAuthorization: bearer <token>で含まれているので、トークンの部分だけ取り出しで検証をします。暗号化作業の時と同じで第一引数にトークン第二引数に秘密鍵、第三引数はトークンの検証を行うかなのでTrueにします。第四引数はアルゴリズムの種類を指定します。
def auth_header
request.headers['Authorization']
end
def decoded_token
if auth_header
token = auth_header.split(' ')[1]
begin
JWT.decode(token, 'my_secret', true,algorithm: 'HS256')
rescue JWT::DecodeError
[]
end
end
end
次回はクライアント側の処理について説明していきます。