今回達成すること
今回はTokenizable(トークナイザブル)
モジュールを作成します。
このモジュールは、トークンの発行と発行主の検索を行います。
User
クラスからインクルードすることで
- ユーザーのトークンを発行する場合は =>
user.to_token
- トークンからユーザーを検索する場合は=>
User.from_token(token)
のように、User
クラスからトークンを扱えるようになります。
モジュールファイルを作成する
まずは「app/services」ディレクトリの「user_auth」ディレクトリ直下に
root $ touch api/app/services/user_auth/tokenizable.rb
作成後の「user_auth」ディレクトリ内はこのようになっています。
api/app/services/user_auth
├── auth_token.rb
├── authenticator.rb
└── tokenizable.rb
Tokenizableモジュールを作成する
まずは、UserAuth
を継承したモジュールを宣言します。
api/app/services/user_auth/tokenizable.rb(以下、省略)
module UserAuth
module Tokenizable
end
end
Userクラスからメソッドを呼び出す用意
User
クラスから、Tokenizable
モジュールのメソッドを呼び出せるようにself.included(インクルーディッド)
メソッドを作成します。
module UserAuth
module Tokenizable
# 追加
def self.included(base)
base.extend ClassMethods
end
## class method
module ClassMethods
end
# ここまで
end
end
-
self.included
... クラスにモジュールが含まれている場合、このメソッドが実行される。User
クラスでTokenizable
をインクルードした時にbase.extend ClassMethods
が実行されます。 -
base.extend ClassMethods
...ClassMethods
をextend
する。これにより、
User
クラスにClassMethods
モジュール内のメソッドが追加されます。
self.includedのまとめ
-
include
=> クラスのインスタンスにメソッドが追加され、 -
extend
=> クラスメソッドが追加されます。参考: RailsTips
つまり、User
クラス内でinclude UserAuth::Tokenizable
を実行した時に、user
インスタンスとUser
クラスにTokenizable
のメソッドが追加されます。
クラスメソッドを作成する
User
クラスから呼び出すクラスメソッドには、渡されたトークンからユーザーを検索するfrom_token
メソッドを追加します。
module UserAuth
module Tokenizable
def self.included(base)
base.extend ClassMethods
end
## class method
module ClassMethods
# 追加
def from_token(token)
auth_token = AuthToken.new(token: token)
from_token_payload(auth_token.payload)
end
private
def from_token_payload(payload)
find(payload["sub"])
end
# ここまで
end
end
end
from_token_payload
... トークンをデコードしたpayload
のユーザーID(sub
)からユーザーを検索し返す。
インスタンスメソッドを作成する
user
インスタンスから呼び出すメソッドには、ユーザーのトークンを返すto_token
メソッドを追加します。
...
private
def from_token_payload(payload)
find(payload["sub"])
end
end
# 追加
## instance method
# トークンを返す
def to_token
AuthToken.new(payload: to_token_payload).token
end
private
def to_token_payload
{ sub: id }
end
# ここまで
end
end
以上で、トークンの発行とデコードを行うメソッドが作成できました。
有効期限付きのトークンを返すメソッドの作成
会員登録時のメール認証やパスワードリセットなどは、セキュリティを考慮してトークンの有効期限を短めに設定する必要があります。
その際に使用する「有効期限付きのトークン」を発行するto_lifetime_token
メソッドを作成します。
api/app/services/user_auth/tokenizable.rb
...
def to_token
AuthToken.new(payload: to_token_payload).token
end
# 追加
# 有効期限付きのトークンを返す
def to_lifetime_token(lifetime)
auth = AuthToken.new(lifetime: lifetime, payload: to_token_payload)
{ token: auth.token, lifetime_text: auth.lifetime_text }
end
private
...
このメソッドは、トークン(token
)と有効期限の日本語テキスト(lifetime_text
)を返します。
lifetime_text
は、「2時間」などの文字列を返します。
この文字列は、メール本文やフロントに渡す「2時間以内に下記URLへアクセスしてください」のようなメッセージに埋め込む際に使用します。
AuthTokenクラスにlifetime_textメソッドを追加する
lifetime_text
メソッドを
api/app/services/user_auth/auth_token.rb
....
def entity_for_user
User.find @payload["sub"]
end
# 追加
# token_lifetimeの日本語変換を返す
def lifetime_text
time, period = @lifetime.inspect.sub(/s\z/,"").split
time + I18n.t("datetime.periods.#{period}", default: "")
end
private
...
-
@lifetime.inspect.sub(/s\z/,'').split
-
inspect
... オブジェクトを人間が読める形式に変換した文字列を返す。2.hoursを「"2 hours"」に変換します。
-
sub(/s\z/,'')
... "2 hours"から「s」を削除し、「"2 hour"」に変換しています。 -
split
... 「"2 hour"」を空白で区切り、配列にしています。["2", "hour"]
以上で、
time
には「"2"」の文字列、period
には「"hour"」の文字列が入ります。 -
-
I18n.t(パス, default: デフォルト値)
...「"hour"」を日本語変換するために、 i18nの日本語化ファイルを呼び出しています。何もなければ、空の文字列を返すようデフォルト値(
default: ""
)を指定しています。
i18nの日本語化ファイルに期間を追加する
i18nの日本語化ファイルperiods
を追加します。
datetime
の下でerrors:
の上あたりに追加してください。
api/config/locales/ja.yml
...
datetime:
...
prompts:
second: 秒
minute: 分
hour: 時
day: 日
month: 月
year: 年
# 追加
periods:
second: 秒
minute: 分
hour: 時間
day: 日
week: 週間
month: ヶ月
year: 年
# ここまで
errors:
...
以上で有効期限付きのトークンを返すメソッドが作成できました。
userモデルでインクルードする
それでは作成したTokenizable
モジュールを
api/app/models/user.rb
require "validator/email_validator"
class User < ApplicationRecord
# include 追加
include UserAuth::Tokenizable
before_validation :downcase_email
...
コンソールで確認してみよう
ユーザークラスで作成したメソッドが使えるかRailsコンソールで確認してみましょう。
root $ docker-compose run --rm api rails c
ユーザーインスタンスからトークンを発行してみます。
> user = User.find(1)
> token = user.to_token
> token
=> "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ey..."
次はクラスメソッドでこのトークンの持ち主を探してみましょう。
> User.from_token(token)
=>
+----+-------+---------------+----------------+-----------+-------+---------------+----------------+
| id | name | email | password_di... | activated | admin | created_at | updated_at |
+----+-------+---------------+----------------+-----------+-------+---------------+----------------+
| 1 | user0 | user0@exam... | $2a$12$rYae... | true | false | 2020-08-25... | 2020-08-25 ... |
+----+-------+---------------+----------------+-----------+-------+---------------+----------------+
無事、ID1を持つユーザーが返されました。
続いて有効期限付きのトークンを発行してみましょう。
> user.to_lifetime_token(2.hours)
=> {:token=>"eyJ0eXAiOiJKV1Q...", :lifetime_text=>"2時間"}
うん、上手く行っていますね。
「ほんまに2時間かぁ?」と思った方はトークンをデコードしてpayload
を確認してみてください。
> token = user.to_lifetime_token(2.hours)
> payload = UserAuth::AuthToken.new(token: token[:token]).payload
> Time.at(payload["exp"])
=> 2020-09-30 17:14:44 +0900
このメソッドを使えば、認証メッセージが簡単に作成できます。
> "#{token[:lifetime_text]}以内に認証してください。"
=> "2時間以内に認証してください。"
確認が取れたらコンソールから抜けましょう。
> exit
以上で今回の作業は終了です。
コミットしとく
コミットしておきましょう。
root $ cd api
api $ git add -A && git commit -m "add_tokenizable.rb" && cd ..
root $
まとめ
今回は、トークンをUser
クラスで簡単に扱えるように、Tokenizable
モジュールを作成しました。
さて次回は?
次回からは、ここまで作成したモジュール群が正しく機能しているか、認証関連一式のテストを行います。
お楽しみじゃ!(GoToBottom↓)