向 ActionCable 发送 auth_token 进行身份验证

Send auth_token for authentication to ActionCable

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 作为动作电缆的终点,所以我想使用 auth_token 进行身份验证。默认情况下,action cable 使用会话用户 ID 进行身份验证。如何将参数传递给连接方法?

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

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

也可以在请求 header 中传递身份验证令牌,然后通过访问 request.headers 哈希来验证连接。 例如,如果身份验证令牌是在名为 'X-Auth-Token' 的 header 中指定的,并且您的用户模型有一个字段 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

有效。但是,最好在您的应用程序中明确要求这些参数。

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

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

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

request.params[:token]

不幸的是,对于 websocket 连接,大多数 2 websocket 客户端不支持额外的 header 和自定义 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)如下所示:

The WebSocket constructor accepts one required and one optional parameter:

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

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

url

The URL to which to connect; this should be the URL to which the WebSocket server will respond.

protocols Optional

Either a single protocol string or an array of protocol strings. These strings are used to indicate sub-protocols, so that a single server can implement multiple WebSocket sub-protocols (for example, you might want one server to be able to handle different types of interactions depending on the specified protocol). If you don't specify a protocol string, an empty string is assumed.

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

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

你也可以看看这个(基本上就是JWT认证+IP地址验证之类的):https://devcenter.heroku.com/articles/websocket-security#authentication-authorization.

万一你们中有人想使用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; 

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

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

正如我在评论中所说,接受的答案是 不是一个好主意 ,只是因为惯例是 URL 不应该 包含此类敏感数据。您可以在此处找到更多信息:https://www.rfc-editor.org/rfc/rfc6750#section-5.3(尽管这是专门针对 OAuth 的)。

然而还有另一种方法:通过 ws url 使用 HTTP 基本身份验证。我发现大多数 websocket 客户端允许您通过在 url 前面加上 http basic auth 来隐式设置 headers:wss://user:pass@yourdomain.com/cable.

这将添加值为 Basic ...Authorization header。在我的例子中,我使用 devise with devise-jwt 并简单地实现了一个继承自 gem 中提供的策略的策略,该策略将 jwt 从 Authorization header 中拉出来。所以我像这样设置 url:wss://TOKEN@host.com/cable 将 header 设置为此(伪):Basic base64("token:") 并在策略中解析它。

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

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

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",
}, //...