#action cableのcurrent_userについて読み解く
##本記事投稿の経緯
Railsで簡単なチャットアプリを作成中、ActionCable内でのcurrent_user
の取り扱いにつまづいたのでメモ。
##環境
Rails 5.2.3(最新じゃないンゴオオオォ)
Ruby 2.4.0(最新じゃないンゴオオオォ)
##connection.rbで定義されるcurrent_user
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
protected
def find_verified_user
remember_token = User.encrypt(cookies[:user_remember_token][:value]) if cookies[:user_remember_token]
if current_user = User.find_by(remember_token: remember_token)
current_user
else
reject_unauthorized_connection
end
end
end
end
上記は公式ドキュメントのサンプルコードであるが、ActionCableはWebSocketで通信する際、まずここで認証を行える。その際、current_user
を定義しているのだが、こいつの仕組みがイマイチ把握できなかったのでソースコードを読み解いていくことにした。
##identified_by
まずはidentified_by
を見ていく。(コードは一部です。)
included do
class_attribute :identifiers, default: Set.new
end
module ClassMethods
# Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
# Common identifiers are current_user and current_account, but could be anything, really.
#
# Note that anything marked as an identifier will automatically create a delegate by the same name on any
# channel instances created off the connection.
def identified_by(*identifiers)
Array(identifiers).each { |identifier| attr_accessor identifier }
self.identifiers += identifiers
end
end
コメントに大体が書いてあるのだが、identifier_by
は後で特定のconnectionを見つけられるようにキーを作っている。今回であればcurrent_user
をキーとしてconnectionを識別できるようにしてくれているのだ。
更にはそれを子であるchannelに自動で同じ名前でdelegate
するという。
Connections form the foundation of the client-server relationship. For every WebSocket accepted by the server, a connection object is instantiated. This object becomes the parent of all the channel subscriptions that are created from there on.
そういえばrails guideにもconnectionはそこから作成されるchannnelの全ての親となると書いてあった。
(これややこしいが継承のことではないっぽい?。、delegate
してるし...それはまた今度、気が向けば記事にします。)
つまり、WebSocketの接続の識別としてcurrent_user
は使われており、更には子であるchannelでも利用可能であるはず。
てわけで使ってみる。
##実際にcurrent_userを使ってみる
###流れ
⓪チャットルームモデルとユーザーモデルは多対多の関係。(ここは省略。)
①connection接続時にユーザーがログイン済みユーザーか認証し、current_user
に接続したユーザーを入れる
②room_channelにサブスクライブする時、先のcurrent_user
にそのチャンネルに接続する権限があるか認証する。
connection.rbは上記のサンプルコードと同じにします。
room_channel.rbのサブスクライブ部分は下記のとおり。
class RoomChannel < ApplicationCable::Channel
def subscribed
if !params.empty? && current_user.is_member?(params['room_id'])
stream_from "room_channel_#{params['room_id']}"
else
reject
end
end
end
クライアントからチャットルームのidを送ってもらい、そのルームにログイン中のユーザーが属しているか確認している。
is_member?
メソッドは下記のような感じで。
def is_member?(room_id)
members = Room.find(room_id).users
members.any?{|member| member.id == self.id }
end
こんな感じでcurrent_user
を使って各チャンネルでも認証をできる。
###is_member?メソッドに指摘が入ったので修正(20190927)
下記はexists?
を使ったver。
ここ、色々書き方あるが、どれが一番リソースに優しいか考えてたら、記事にできそうだったので今度書きます。(SQL力が足りない...)
def is_member?(room_id)
Room.find(room_id).users.exists?(self.id)
end
##まとめ
connection時、ユーザーがログイン済みか認証し、ログインしていればそれをcurrent_user
にいれ、子のチャンネルで利用可能にする。
current_user
を使えばチャンネル内でも認証可能。
##あとがき
実は今回、current_user
の取り扱いでつまづいたのはrspecでテスト中のことでして、
「current_user
はチャンネル内で使えるって聞いたんだけど!!」
とか叫いたりして、
「本当に使えんのか?.....調べてやる!」
と思い執筆しました。
結局、testコードの書き方が間違っていただけでしたンゴ!!めでたしめでたし!!
まあためにはなったかな。。。
##参考記事
【ActionCable】チャンネル接続/購読時にユーザ認証を行う
これはMUST!ActiveSupport の Class#class_attribute を使おう!
Rubyのdelegateについて整理する
Rails Guide