ここまでの記事はこちら
Ruby on Railsの習得のためタスクリスト的なの作ってみる①〜構成、設計編〜
Ruby on Railsの習得のためタスクリスト的なの作ってみる②〜Rails7.0+React18.0+ MySQL8.0 Docker開発環境構築編〜
Ruby on Railsの習得のためタスクリスト的なの作ってみる③〜API開発・呼び出し編〜
Ruby on Railsの習得のためタスクリスト的なの作ってみる④〜API開発・MySQL接続とモデル実装編〜
今回は、認証周りを実装します。
構成
ログインAPIでは、アクセストークンとリフレッシュトークンをJWTで発行します。
アクセストークンはレスポンスボディで返却、リフレッシュトークンはCookieに追加します。
トークン更新APIでは、Cookieから取得したリフレッシュトークンで認証し、アクセストークンとリフレッシュトークンを再発行します。
JWTとは
こちらがわかりやすいかと思います。
簡単に説明すると、HTTPヘッダに情報を付加するためのJSONの形式
であり、一般に
・改竄されにくく、
・改竄を検知できる
ように運用することでログイン情報を保持する用法で使われることの多い形式です。
モデル準備
リフレッシュトークンの発行をイベントとしてテーブルに保存することでユーザーとの紐付けを簡単にし、かつ、同一リフレッシュトークンによるトークン再発行を一度限りとすることを実現します。
class CreateTokenIssueEvents < ActiveRecord::Migration[7.0]
def change
create_table :token_issue_events do |t|
t.references :user, foreign_key: true, null: false
t.text :refresh_token, null: false
t.datetime :event_datetime, null: false
end
end
end
ユーザーテーブルに関しては、password_digest
カラムを追加しておきます。
これにより、ActiveModelのauthenticate
メソッドが使えるようになります。
詳しくはこちらのドキュメントに書いてあります。
また、Userモデルクラスにhas_secure_password
メソッドを追加しておく必要があります。
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :password_digest, null: false
t.string :name, null: false
t.string :mail_address, null: false
end
add_index :users, [:mail_address], unique: true
end
end
ログイン認証
authenticate
メソッドは、一件取得したUserインスタンスに対して呼び出すと、以下のように、パスワードを引数として渡すだけで認証ができてしまいます。
module Api
module V1
class AuthController < ApplicationController
def login
mail_address = params[:mail_address]
password = params[:password]
user = User.find_by(mail_address:)
if user.nil?
# ユーザーが存在しない場合の処理
end
authed_user = user.authenticate(password)
unless authed_user
# パスワードによる認証に失敗した場合の処理
end
# 正常な場合の処理 ここでトークンの発行を行う(次章)
end
end
end
end
トークンの発行
上記リンクのReadmeに従ってgemをインストールすると、JWT.encode
とJWT.decode
を主に使ってJWTのエンコード、デコードができるようになります。
トークンの生成のコード例は以下のようになります。アクセストークンもリフレッシュトークンも同様に生成できます。
また、この例ではRS256アルゴリズムを使っていますが、他の暗号アルゴリズムを使いたい場合は上記リンクのReadmeを参照して下さい。
def generate_access_token(subject)
rsa_private = File.open('file_name_of_rsa', 'r').read
private_key = OpenSSL::PKey::RSA.new(rsa_private)
exipire_access = 60.minute # 有効期限
JWT.encode(
{ iss: config.jwt.issuer,
sub: subject,
exp: Time.current.since(exipire_access).to_i },
private_key,
'RS256'
)
end
トークンの認証
トークン認証のコード例は以下のようになります。
def decode_token(token)
rsa_public = File.open('file_name_of_rsa.pub', 'r').read
public_key = OpenSSL::PKey::RSA.new(rsa_public)
begin
decoded_token = JWT.decode(token, public_key, true, { algorithm: 'RS256' })
rescue JWT::VerificationError => e
# 認証エラー時の処理
rescue JWT::ExpiredSignature => e
# 有効期限切れ時の処理
end
decoded_token
end
以上のトークン生成・認証をいい感じにControllerから呼び出すことでログインAPIやトークン更新APIを実装することができます。
例えば、ログイン認証で認証した後に、以下のように
・リフレッシュトークンをCookieにセット
・トークン更新イベントテーブルにレコード保存
・アクセストークンを返却
するといい感じになります!
access_token = TokenHandler.generate_access_token(authed_user.id)
refresh_token = TokenHandler.generate_refresh_token(authed_user.id)
cookies[:jwt] = refresh_token
token_issue_event = TokenIssueEvent.new(
{ user_id: authed_user.id, refresh_token:, event_datetime: Time.current }
)
token_issue_event.save
render json: { access_token: }
次回予告
これで一応バックエンドのAPIが動くようになったので、次回はReactでフロントを作ります。サクッといきたい...!