向ActionCable发送auth_token以进行身份验证

23
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      #puts params[:auth_token]
      self.current_user = find_verified_user
      logger.add_tags 'ActionCable', current_user.name
   end

  end
end

我不想将Web用作Action Cable的端点,因此我想使用auth_token进行身份验证。默认情况下,Action Cable使用会话用户ID进行身份验证。 如何将参数传递给connect方法?


1
官方文档在此处提供了一个带有URL令牌的示例:https://guides.rubyonrails.org/action_cable_overview.html#connect-consumer - Henrik N
10个回答

54

我成功地将我的认证令牌作为查询参数发送了。

在我的JavaScript应用程序中创建消费者时,我将令牌作为电缆服务器URL的参数传递,就像这样:

wss://myapp.com/cable?token=1234

在我的有线连接中,我可以通过访问request.params来获取这个token

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
      logger.add_tags 'ActionCable', current_user.name
    end

    protected:
    def find_verified_user
      if current_user = User.find_by(token: request.params[:token])
        current_user
      else
        reject_unauthorized_connection
      end
    end
  end
end

显然这不是理想的情况,但我认为在创建WebSocket时无法发送自定义标题。


2
当我找到你的答案时,我几乎已经在打这个了。谢谢! - djvs
有其他人成功连接了吗?我的还是无法连接。当您在“cable server URL”中传递令牌时,这是在development.rb或production.rb中进行的,对吗? - Raidspec
更新:提醒其他人注意:您需要将“wss://myapp.com/cable?token=1234”放入您的cable.js文件中。 - Raidspec
1
我想知道这种方法是否安全。让我担心的是,通常没人使用普通的HTTP请求来做这件事,但如果这样做没有问题,那为什么呢? - Joe Half Face
1
虽然这是关于OAuth的,但您可以在此处找到为什么不应采用此方法的良好摘要:https://tools.ietf.org/html/rfc6750#section-5.3。我将添加自己的答案,介绍我如何实现身份验证。 - phikes
显示剩余6条评论

17
Pierre的回答是有效的。不过,在应用程序中明确地期望这些参数是一个好主意。
例如,您可以在其中一个配置文件(例如application.rbdevelopment.rb等)中执行此操作:
config.action_cable.mount_path = '/cable/:token'

然后只需要通过您的Connection类访问它:

request.params[:token]

应该是/cable?:token而不是/cable/:token,我想令牌是作为查询参数传递而不是实际路由。 - Masroor

10

不幸的是,大多数websocket客户端和服务器都不支持附加额外的头信息和自定义头信息1。因此,可能的选择是:

  • 将其作为URL参数附加并在服务器上解析

path.to.api/cable?token=1234

# and parse it like
request.params[:token]

缺点: 如果JWT被记录在日志和系统进程信息中,则可能会存在漏洞,供其他具有服务器访问权限的人使用,更多信息请参见 这里

解决方案: 对令牌进行加密并附加,即使在日志中可以看到它,也没有任何作用,除非其解密。

  • 将JWT附加在允许的参数之一中。

客户端:

# Append jwt to protocols
new WebSocket(url, existing_protocols.concat(jwt))

我创建了一个名为action-cable-react-jwt的JS库,适用于ReactReact-Native,它可以轻松完成此任务。随意使用。

服务器端:

# get the user by 
# self.current_user = find_verified_user

def find_verified_user
  begin
    header_array = self.request.headers[:HTTP_SEC_WEBSOCKET_PROTOCOL].split(',')
    token = header_array[header_array.length-1]
    decoded_token = JWT.decode token, Rails.application.secrets.secret_key_base, true, { :algorithm => 'HS256' }
    if (current_user = User.find((decoded_token[0])['sub']))
      current_user
    else
      reject_unauthorized_connection
    end
  rescue
    reject_unauthorized_connection
  end
end

1 大多数Websocket API(包括Mozilla的)都像下面这个例子一样:

WebSocket构造函数接受一个必需参数和一个可选参数:

WebSocket WebSocket(
  in DOMString url,
  in optional DOMString protocols
);

WebSocket WebSocket(
  in DOMString url,
  in optional DOMString[] protocols
);

url

连接的URL;这应该是WebSocket服务器将响应的URL。

protocols 可选

一个协议字符串或协议字符串数组。这些字符串用于指示子协议,以便单个服务器可以实现多个WebSocket子协议(例如,您可能希望一个服务器能够处理根据指定的协议类型不同类型的交互)。如果您没有指定协议字符串,则假定为空字符串。

2 总有例外,例如这个node.js库ws允许构建自定义标题,因此您可以使用常规的Authorization: Bearer token标题,并在服务器上解析它,但客户端和服务器都应使用ws


6

如我在评论中所述,被接受的答案是不好的想法,因为约定是URL不应该包含敏感数据。您可以在此处找到更多信息:https://www.rfc-editor.org/rfc/rfc6750#section-5.3(尽管这是特别针对OAuth的)。

然而,还有另一种方法:通过ws URL使用HTTP基本认证。我发现大多数websocket客户端允许您通过在url前加上http基本认证来隐式设置标头,如下所示:wss://user:pass@yourdomain.com/cable

这将添加一个值为Basic ...Authorization标头。在我的情况下,我正在使用devisedevise-jwt,只需实现从宝石提供的继承策略中提取jwt的策略即可。所以我像这样设置了url:wss://TOKEN@host.com/cable,它将标头设置为(伪):Basic base64("token:")并在策略中解析。


好的解决方案,绝对正确:敏感信息不应该成为URL的一部分。你介意分享一下这个策略的要点链接吗?我很想看看你的实现是什么样子的。我也在使用devise-jwt并尝试解决这个问题。 - subvertallchris
最终我发现这种方法太过于hacky,于是转而使用另一种解决方案。我为授权构建了一个专用的操作。它设置了当前用户。其他操作然后检查是否已设置此操作,否则拒绝。因此,您必须首先进入通道,调用auth操作,然后调用任何经过身份验证的操作。我将其封装到我的ApplicationChannel中。@subvertallchris,这有意义吗? - phikes
啊,我都忘了我之前还回答过这个问题了。https://dev59.com/yFsW5IYBdhLWcg3wCDTF#53007956 - phikes
是的,那很有道理,我完全同意。我尝试了一些基本的身份验证方法,但都不太合适,所以我也选择了你所描述的方法。感谢你的跟进! - subvertallchris

1
另一种方法(最终我使用的方法,而不是我的其他答案)是在您的频道上拥有一个authenticate操作。 我使用这个来确定当前用户并将其设置在连接/频道中。 所有的东西都通过Websockets发送,所以当我们加密它时(即wss),凭据就不是问题了。

1

补充之前的答案,如果您将JWT用作参数,则至少需要在JS中使用btoa(your_token),在Rails中使用Base64.decode64(request.params[:token])进行解码,因为Rails认为点“.”是分隔符,所以您的令牌将在Rails参数端被截断。


1

最近有人问我这个问题,我想分享一下目前我在生产系统中使用的解决方案。

class MyChannel < ApplicationCable::Channel
  attr_accessor :current_user

  def subscribed
    authenticate_user!
  end

  private

  # this works, because it is actually sends via the ws(s) and not via the url <3
  def authenticate_user!
    @current_user ||= JWTHelper.new.decode_user params[:token]

    reject unless @current_user
  end
end

然后可以重新使用守卫策略来处理该JWT(并让它处理所有可能的边缘情况和问题)。
class JWTHelper
  def decode_user(token)
    Warden::JWTAuth::UserDecoder.new.call token, :user, nil if token
  rescue JWT::DecodeError
    nil
  end

  def encode_user(user)
    Warden::JWTAuth::UserEncoder.new.call(user, :user, nil).first
  end
end

尽管我没有在前端使用ActionCable,但它应该大致如下工作:
this.cable.subscriptions.create({
  channel: "MyChannel",
  token: "YOUR TOKEN HERE",
}, //...

1

如果有人想使用ActionCable.createCustomer,但是像我一样需要可更新的令牌:

const consumer = ActionCable.createConsumer("/cable")
const consumer_url = consumer.url
Object.defineProperty(
  consumer, 
  'url', 
  {
      get: function() { 
        const token = localStorage.getItem('auth-token')
        const email = localStorage.getItem('auth-email')
        return consumer_url+"?email="+email+"&token="+token
      }
  });
return consumer; 

如果连接丢失,将使用全新的令牌重新打开连接。


0
关于Pierre's answer的安全性:如果您正在使用WSS协议,该协议使用SSL进行加密,则发送安全数据的原则应与HTTPS相同。在使用SSL时,查询字符串参数以及请求正文都会被加密。因此,如果在HTTP API中通过HTTPS发送任何类型的令牌并认为它是安全的,则对于WSS也应该是相同的。只需记住,与HTTPS一样,不要通过查询参数发送凭据(如密码),因为请求的URL可能会在服务器上记录,从而存储您的密码。而是使用由服务器发行的令牌等内容。
另外,您可以查看这个(基本上描述了类似JWT身份验证+ IP地址验证的内容):https://devcenter.heroku.com/articles/websocket-security#authentication-authorization

-1

还可以通过请求头传递身份验证令牌,然后通过访问request.headers哈希来验证连接。

例如,如果身份验证令牌在名为“X-Auth-Token”的标头中指定,并且您的用户模型具有一个字段auth_token,则可以执行以下操作:

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
      logger.add_tags 'ActionCable', current_user.id
    end

    protected

    def find_verified_user
      if current_user = User.find_by(auth_token: request.headers['X-Auth-Token'])
        current_user
      else
        reject_unauthorized_connection
      end
    end
  end
end

遗憾的是,无法为WebSocket连接设置标头。因此,这个答案实际上是误导性和无效的 :/ - acorncom
如果是这样,那么如何在iOS中使用Action Cable呢? @acorncom - Raidspec
@Raidspec,我认为你需要采用上面概述的查询哈希方法。 - acorncom
@acorncom,抱歉打扰了,但我认为你并不完全正确。事实上,握手是通过常规的HTTP请求进行的。使用faye-websocket gem,在rack中处理websocket连接,我成功地获取了头部信息,授权用户,然后打开/关闭连接,一切都正常工作。不确定是否可以使用action cable实现。 - Joe Half Face
2
嗨,乔,根据我之前提出的解决方案,这实际上是可能的。 对于纯Websocket连接来说,原则上无法设置标头,但在ActionCable中实际上是可以的。并且它们可以在Cable Connection端被提取。 - rikettsie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接