向 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.rb
、development.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))
我为 React
和 React-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",
}, //...
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.rb
、development.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))
我为 React
和 React-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
OptionalEither 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
.
至于
你也可以看看这个(基本上就是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",
}, //...