最近、IDの可逆な方式で難読化したいけどfriendly_id
みたいなmigrationが伴うものは、ちょっと避けたかったので色々と方法を考えたり人に教えてもらったりしたので、いろいろ方法とかをメモしておきます。
やりたいこと
今回は「ユーザーにコンテンツ紹介メールを送信して、そのメールに記載されたURLをクリックしたときにブックマークさせる」 という機能で考えてみます。
普通に考えると下記のような実装になるかなと思うのですが、この実装には問題点があります。
# メール文面 # このコンテンツをブックマークしませんか? # https://example.com/page/:id/bookmark Rails.application.routes.draw do resouces :pages do member { get 'bookmark' } end end class User; has_many :bookmarks; end class Page; has_many :bookmarks; end class Bookmark belongs_to :user belongs_to :page end class PagesController < ApplicationController def bookmark page = Page.find(params[:id]) if current_user.bookmarks.create(page: page) flash[:notice] = 'ブックマークに成功しました!' else flash[:alert] = 'ブックマークに失敗しました!' end end end
それは、idが予測可能なためユーザーがURLを予測して独自に拡散等を行うと他ユーザーに不正に任意のページをブックマークさせるようなことが出来てしまいます。
これに対応するためにはidの難読化させて予測不能にする対応が考えられます。
ActiveRecordでUUIDを使う
まず、ActiveRecordのUUIDサポートを使うことが考えられます。 実装も特に工夫する必要もなくIDを予測不能にする必要が最初から検討できていれば良い方法かなと思います。
しかし、すでにidをintで作ってしまっていることや、Posgresqlでしか使えないのでMySqlでは使うことは出来ないといった問題点があります。
ActiveSupport::MessageEncryptorを使う
ActiveSupport::MessageEncryptor
を使って可逆暗号化してIDを難読化する方法も考えられます。
実装は下記のような感じになるかなと思います。
class Page ENCRYPT_CIPHER = "aes-256-cbc" ENCRYPT_SECRET = '01234567890123456789012345678901' has_many :bookmarks def self.find_with_encrypted_id(encrypted_id) crypt = ActiveSupport::MessageEncryptor.new(ENCRYPT_SECRET, cipher: ENCRYPT_CIPHER) decrypted_id = crypt.decrypt_and_verify(decrypt_and_verify) find(decrypted_id) end def encrypted_id crypt = ActiveSupport::MessageEncryptor.new(ENCRYPT_SECRET, cipher: ENCRYPT_CIPHER) crypt.encrypt_and_sign(id) end end class PagesController < ApplicationController def bookmark page = Page.find_with_encrypted_id(params[:id]) if current_user.bookmarks.create(page: page) flash[:notice] = 'ブックマークに成功しました!' else flash[:alert] = 'ブックマークに失敗しました!' end end end
bookmark_page_url(id: page.encrypted_id)
のような形でhttps://example.com/page/NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh.../bookmark
のようなURLを発行して、その難読化されたIDで検索してブックマークを作成することが出来ます。
しかしActiveSupport::MessageEncryptor
で暗号化した文字列を生成すると同じ値を暗号化しても毎回違う値が生成されます。(※復号結果は変わらない)
なので同一リソースを表すURLが毎回変わってしまうのはうーんという感じがしますね・・・。
scatter_swapを使う
scatter_swap
というgemを使うと数字を10桁の数字に難読化することが出来ます。
このgemを使うを下記のような形でIDの難読化と復号を行う事ができます、シンプル✨
class Page has_many :bookmarks def self.find_with_encrypted_id(encrypted_id) decrypted_id = ScatterSwap.reverse_hash(encrypted_id).to_i find(decrypted_id) end def encrypted_id ScatterSwap.hash(id).to_i end end
bookmark_page_url(id: page.encrypted_id)
のような形でhttps://example.com/page/4517239960/bookmark
のようなURLを発行して、その難読化されたIDで検索してブックマークを作成することが出来ます。
しかしidが数字だということはバレてしまう + 最終更新が7年前とメンテされる気配があやしそう・・・😭
hashid-railsを使う
hashid-rails
を使っても同様のことが出来ますし、かつかなりシンプルに実装出来ます(これがいいのでは?)
class Page include Hashid::Rails has_many :bookmarks end class PagesController < ApplicationController def bookmark page = Page.find(params[:id]) if current_user.bookmarks.create(page: page) flash[:notice] = 'ブックマークに成功しました!' else flash[:alert] = 'ブックマークに失敗しました!' end end end
bookmark_page_url(id: page.hashid)
のような形でhttps://example.com/page/yLA6m0oM/bookmark
のようなURLを発行して、その難読化されたIDで検索してブックマークを作成することが出来ます。
英語なので数字よりも元のIDを予測しずらく安全性も高そうですね✨そこそこメンテナンスもされてそうです🙌
結論
今回のケースだとhashid-rails
が個人的には良さそうな気がしました🙌
イケてるgemをしっておくとこういうときに便利なので色々調べておきたいなと思いました🙇♂️