向 ActionCable 发送 auth_token 进行身份验证

Posted

技术标签:

【中文标题】向 ActionCable 发送 auth_token 进行身份验证【英文标题】:Send auth_token for authentication to ActionCable 【发布时间】:2016-05-31 19:50:56 【问题描述】:
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

我不使用网络作为动作电缆的端点,所以我想使用 auth_token 进行身份验证。默认情况下,action cable 使用会话用户 ID 进行身份验证。如何将参数传递给连接方法?

【问题讨论】:

官方文档在此处的 URL 中有一个带有令牌的示例:guides.rubyonrails.org/… 【参考方案1】:

我设法将我的身份验证令牌作为查询参数发送。

在我的 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 时发送自定义标头。

【讨论】:

当我找到你的答案时,我实际上是在输入这个。谢谢! 还有其他人让它工作吗?我的仍然没有连接到这个。当您在“有线服务器 URL”中传递令牌时,这是 development.rb 或 production.rb 中的部分,对吧? 更新:提醒其他人:您将把“wss://myapp.com/cable?token=1234”放在您的 cable.js 文件中。 我想知道这种方法是否安全。让我担心的是,没有人对通常的 HTTP 请求这样做,但如果没问题,那为什么? 虽然这是关于 OAuth 的,但您可以在这里找到一个很好的总结,说明为什么不应该采用这种方法:tools.ietf.org/html/rfc6750#section-5.3。我将添加我自己的关于如何实现身份验证的答案。【参考方案2】:

Pierre's answer 有效。但是,最好明确说明在您的应用程序中期望这些参数。

例如,在您的一个配置文件(例如application.rbdevelopment.rb 等...)中,您可以这样做:

config.action_cable.mount_path = '/cable/:token'

然后只需从您的 Connection 类中访问它:

request.params[:token]

【讨论】:

不应该是/cable?:token 而不是/cable/:token 吗?我想令牌是作为查询参数出现的,而不是作为实际路线【参考方案3】:

不幸的是,对于 websocket 连接,大多数2 websocket 客户端和服务器不支持附加标头和自定义标头1。 所以可能的选择是:

附加为 URL 参数并在服务器上解析

path.to.api/cable?token=1234

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

缺点:它可能容易受到攻击,因为它可能最终出现在日志和系统进程信息中,可供有权访问服务器的其他人使用,更多 here

解决方案:加密令牌并附加它,因此即使可以在日志中看到它,在解密之前它也没有任何用处。

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

客户端:

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

我为 ReactReact-Native 创建了一个 JS 库 action-cable-react-jwt,它就是这样做的。随意使用。

服务器端:

# 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's)和下面的一样:

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

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

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

url

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

protocols 可选

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

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

【讨论】:

【参考方案4】:

正如我在评论中所说,接受的答案是不是一个好主意,因为惯例是 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 标头。在我的情况下,我使用 devise 和 devise-jwt 并简单地实现了一个从 gem 中提供的策略继承的策略,该策略将 jwt 从 Authorization 标头中提取出来。因此,我将 url 设置为:wss://TOKEN@host.com/cable 将标头设置为此(伪):Basic base64("token:") 并在策略中对其进行解析。

【讨论】:

很好的解决方案,绝对正确:敏感信息不应成为 URL 的一部分。您是否介意分享指向该策略要点的链接?很想看看你的实现是什么样的。我也在使用 devise-jwt 并试图解决这个问题。 最后我发现这太hacky并切换到另一个解决方案。我建立了一个专门的授权动作。它设置 current_user。然后其他操作检查是否已设置,否则拒绝。所以你必须先进入频道,调用 auth 动作,然后调用任何经过身份验证的动作。我将它封装到我的 ApplicationChannel 中。这有意义吗,@subvertallchris? 啊,我忘了我什至对此做了另一个回答。 ***.com/a/53007956/1218081 是的,这很有道理,我完全同意。我探索了一些基本的身份验证方法,但没有一个感觉正确,所以我也确定了你所描述的内容。感谢您的跟进!【参考方案5】:

如果您想使用 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; 

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

【讨论】:

【参考方案6】:

要添加到以前的答案,如果您使用 JWT 作为参数,您将必须至少 btoa(your_token) @js 和 Base64.decode64(request.params[:token]) @rails 因为 rails 认为 dot '.'分隔符,因此您的令牌将被切断@rails 参数侧

【讨论】:

【参考方案7】:

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

【讨论】:

【参考方案8】:

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

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

然后重新使用 Warden 策略来处理该 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",
, //...

【讨论】:

【参考方案9】:

还可以在请求标头中传递身份验证令牌,然后通过访问 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 中如何使用 actioncable? @Raidspec 我相信您需要采用上面概述的查询哈希方法 @acorncom,抱歉发布了 necroposting,但我不认为你是绝对正确的。问题是握手是通过通常的 HTTP 请求进行的。使用在机架中处理 websocket 连接的 faye-websocket gem,我设法获取标题,授权用户,然后打开/关闭连接,它可以正常工作。不确定是否可以使用动作电缆 嗨,乔,按照我之前提出的解决方案,这实际上是可能的。对于纯 websocket 连接,标头无法设置 - 原则上,但在 ActionCable 中您实际上可以。并且可以在电缆连接端提取它们。【参考方案10】:

至于Pierre's answer 的安全性:如果您使用WSS 协议,它使用SSL 进行加密,那么发送安全数据的原则应该与HTTPS 相同。使用 SSL 时,查询字符串参数和请求正文一样被加密。因此,如果在 HTTP API 中您通过 HTTPS 发送任何类型的令牌并认为它是安全的,那么对于 WSS 应该是相同的。请记住,与 HTTPS 相同,不要通过查询参数发送密码等凭据,因为请求的 URL 可能会记录在服务器上并因此与您的密码一起存储。而是使用服务器颁发的令牌之类的东西。

你也可以看看这个(这基本上描述了 JWT 身份验证 + IP 地址验证之类的东西):https://devcenter.heroku.com/articles/websocket-security#authentication-authorization。

【讨论】:

以上是关于向 ActionCable 发送 auth_token 进行身份验证的主要内容,如果未能解决你的问题,请参考以下文章

ActionCable:检测客户端连接丢失,向用户显示连接状态

使用ActionCable部分错误地呈现

如何使用 ActionCable 作为 API

如何使用 ActionCable 在 Rails( api-only ) 应用程序中主动发送消息?

ActionCable - 响应错误

如何在 minitest 中模拟和验证 ActionCable 传输?