LoginSignup
4

More than 3 years have passed since last update.

ActionCableにおいてのcurrent_userについて読み解く|Rails

Last updated at Posted at 2019-09-27

action cableのcurrent_userについて読み解く

本記事投稿の経緯

Railsで簡単なチャットアプリを作成中、ActionCable内でのcurrent_userの取り扱いにつまづいたのでメモ。

環境

Rails 5.2.3(最新じゃないンゴオオオォ)
Ruby 2.4.0(最新じゃないンゴオオオォ)

connection.rbで定義されるcurrent_user

connection.rb
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を見ていく。(コードは一部です。)

action_cable/connection/identification.rb
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のサブスクライブ部分は下記のとおり。

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?メソッドは下記のような感じで。

user.rb
  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力が足りない...)

user.rb
  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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4