こんにちは、サーバーサイドエンジニアの花田です。
業務では主に管理画面系の開発をしています。
今回は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を導入したいところ
- Vue.jsとRailsがAPIでやり取りを行うとき (GET)
- 外部サービスと連携しているので、外部サービスのjsとRailsがAPIでやり取りを行うとき (POST)
Vue.jsとRails間のJWTについて
APIを用いてDBから必要なデータを取得し、管理画面にデータを表示させたい
APIの例
1.DBからデータを取得するAPI https://localhost/api/v1/samples
処理の流れ(GET)
- RailsはsessionのユニークIDを元にJWTのエンコードを行う
- VuexにJWTトークンを保持する
- Vue.jsは、VuexにあるJWTトークンをAPIに付与して実行する
- RailsはJWTトークンをデコードして以下のバリデーションを行う
- JWTトークンが正しい値であること
- 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)
- 外部サービスのjs内で、JWTトークンを取得するAPIを実行
- APIのパラメータに管理者用のユニークIDを付与する
- ユニークIDが正しいかバリデーションを行い、正しい場合はJWTトークを返す
- JWTトークンをAPIに付与して実行する
- 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間の依存もなくなりスッキリしました。
まとめ
外部サービスと連携して、認証機能を実装したことがなかったのでいい経験になりました。
まだリファクタリングしたい箇所があるので、可読性をもっとあげていきたいと思っています。
最後まで読んでいただき、ありがとうございました!