Rails + Vue.js + Vuexの構成にJWTを入れてみた

こんにちは、サーバーサイドエンジニアの花田です。

業務では主に管理画面系の開発をしています。
今回はAPIにJWTを組み込んだ話を書こうと思います。

構成

Rails(5.2.2) + Vue.js(2.5.16) + Vuex(3.0.1)

JWTとは?

JSON Web Token の略です。JSON形式で通信を安全に行うための仕組みです。

例 トークン

eyJhbGciOiJIUzI1NiJ32eyJzdGFmZl9pZCI6IjYyNiIsImV4edI6MTU2OTQ5ODIwOH0.V7ahesLTgGSAC6352g6kVqr_jzhykV3Jc0UH32l9djg

JWTの導入方法

gemでインストールを行えば、JWTを入れることができます。
業務ではbundleを使用しているので、bundle経由のコマンド例です。

# Gemfile
...
gem 'jwt'
...
bundle install

JWTを導入したいところ

  1. Vue.jsとRailsがAPIでやり取りを行うとき (GET)
  2. 外部サービスと連携しているので、外部サービスのjsとRailsがAPIでやり取りを行うとき (POST)

Vue.jsとRails間のJWTについて

APIを用いてDBから必要なデータを取得し、管理画面にデータを表示させたい

APIの例

1.DBからデータを取得するAPI
https://localhost/api/v1/samples

処理の流れ(GET)

  1. RailsはsessionのユニークIDを元にJWTのエンコードを行う
  2. VuexにJWTトークンを保持する
  3. Vue.jsは、VuexにあるJWTトークンをAPIに付与して実行する
  4. RailsはJWTトークンをデコードして以下のバリデーションを行う
    1. JWTトークンが正しい値であること
    2. sessionのユニークIDがユーザーのログイン情報と一致していること

その後、Railsのバリデーションが通れば、無事管理画面にデータが表示されるようになります。

外部サービスとRails間のJWTについて

外部サービスから必要なデータを送ってDBを更新させたい

APIの例

1.JWTのトークンを取得するAPI
 http://localhost/api/v1/get_jwt_tokens
2.DBにデータを保存するAPI
 http://localhost/api/v1/external_service/samples

処理の流れ(POST)

  1. 外部サービスのjs内で、JWTトークンを取得するAPIを実行
  2. APIのパラメータに管理者用のユニークIDを付与する
  3. ユニークIDが正しいかバリデーションを行い、正しい場合はJWTトークを返す
  4. JWTトークンをAPIに付与して実行する
  5. Rails側で必要なデータが更新される

JWTコード例

例 エンコード

JWT.encode(
  {
    ユニークID: 1,
    exp:       (Time.now.in_time_zone + 5.minutes).to_i
  },
  ユニーク値
)

例 デコード

JWT.decode(
  eyJhbGciOiJIUzI1NiJ32eyJzdGFmZl9pZCI6IjYyNiIsImV4edI6MTU2OTQ5ODIwOH0.V7ahesLTgGSAC6352g6kVqr_jzhykV3Jc0UH32l9djg,
  ユニーク値
)

※ 実際のコードには、ユニーク値に「Rails.application.credentials.secret_key_base」を入れています。

大変だったところ

その1

Vue.jsとRails間のJWT処理、外部サービスとRails間のJWT処理を親クラスに実装したので同じようなコードが増えた
処理がほぼ変わらない名前違いのメソッドが増えた、、

親クラス

app/controllers/api_controller.rb

同じようなメソッド

def auth_token
・
・
・
end

def auth_token_param
・
・
・
end

その2

認証時のバリデーションがAPIによって違う
※ API1ではuidが正しいか判断、API2ではsessionのIDと同じか判断などなど、、

例 バリデーション

1. auth_token[:uid].present?

2. auth_token[:user_id] == session['user_id']

その3

JWTトークンを付与する方法が違う
通常はheaderに付与するが、外部サービスの仕様によりheaderで送れないのでparamsで送ることになった

例 JWTトークンをコントローラーで取得する際

1. params['Authorization-BearerHeader']

2. request.headers['Authorization'].try(:split, ' ').try(:last)

このようなに煩雑なコードが増えました。。。。。

対策

親クラスからJWT処理を取り出して、各APIで必要なJWTをincludeする方法に変更しました。

以下のように用途によってクラスを分けました。

例 用途によってクラスを分ける 

lib/authentication/jwt/outside/header.rb
lib/authentication/jwt/outside/param.rb
lib/authentication/jwt/inside/header.rb
lib/authentication/jwt/inside/admin_header.rb

例 ある外部APIにJWTをincludeする

include Authentication::Jwt::Outside::Header

こうすることで、親クラスにはJWTのコードがなくなり、各JWT間の依存もなくなりスッキリしました。

まとめ

外部サービスと連携して、認証機能を実装したことがなかったのでいい経験になりました。
まだリファクタリングしたい箇所があるので、可読性をもっとあげていきたいと思っています。
最後まで読んでいただき、ありがとうございました!