跨应用/服务器的Rails身份验证

31

我一直在开发我的Rails应用程序,同时尽可能保持其模块化。我试图将不同的部分实现为服务。

以Facebook为例:
a)一个称为MainApp的应用程序,允许用户拥有墙、帖子等功能。
b)一个称为PhotoApp的应用程序,存储照片,允许用户查看自己的照片等功能。这是一个独立的应用程序,将有一个REST API可以供MainApp使用。

我想使用OAuth作为单点登录解决方案(如此教程所述http://blog.joshsoftware.com/2010/12/16/multiple-applications-with-devise-omniauth-and-single-sign-on/),其中每个应用程序都将通过OAuth进行授权,并根据cookie获取对当前用户会话的访问权限。

第一个问题:这是可行的解决方案吗?

第二个问题:我希望能够从MainApp服务器调用PhotoApp的API(而不是从用户的浏览器中)。在这种情况下,认证如何工作?

第三个问题:如果我有一个使用node.js的服务,它会如何工作?


有一些Oauth服务器可以帮助实现这样的场景。 - Thomas Klemm
3个回答

20

是的,使用OAuth进行SSO是可行的解决方案,但不是最简单的。在构建任何新东西时,OAuth 2.0是首选。OAuth标准涵盖了很多内容。

OAuth的主要优点是它允许用户向第三方应用程序授权而不向第三方透露其密码。如果你没有认真提供这样的互操作性,那么OAuth可能过于复杂。

考虑到复杂性,我提供了一对不同的解决方案:

对于单点登录

诀窍在于在域内的主机之间共享会话ID cookie,并使用共享的会话存储(如ActiveRecordStore或基于缓存的存储)。

每个Rails应用都有一个用于签署cookie的“秘密”。在较新的Rails应用中,这位于/config/initializers/secret_token.rb中。在每个应用程序中设置相同的秘密令牌。

然后,配置会话以允许从所有子域访问:

AppName::Application.config.session_store :active_record_store, :key => '_app_name_session', :domain => :all

内部API调用

使用良好的共享密钥在HTTPS连接上进行身份验证。将密钥传递到“Authorization”标头值中。

您可以轻松地与其他架构(如node.js)一起使用共享密钥。只需确保始终使用HTTPS,否则共享密钥可能会被网络嗅探。


1
共享会话ID似乎是SSO的一个不错的起点,但仅止于此。这里留下了大量的实现细节。应用程序在登录期间如何进行身份验证?应用程序如何了解已登录用户的能力? - Fitzsimmons
1
是的,在实现完整的SSO解决方案时需要进行大量设计。创建一个中央认证服务可能是一个不错的开始,但这已经超出了StackOverflow上的答案...可能更像一本O'Reilly书。 - Mars
这假设所有域名共享相同的顶级域名/子域名。那不同的顶级域名呢? - lukad
这个答案并非假设服务器都在同一域中,它明确表述了这一事实(见“单点登录”下)。如果服务器分布在不同的域中,则需要研究更复杂的解决方案,例如OAuth或SAML-SSO。 - Mars

5
你可以查看 Jeremy Green 在 2014 年 RailsConf 提出的基于服务导向架构(Service Oriented Architecture)的解决方案。
这篇博客文章包含所有资源(仓库、演示等),链接在这里:http://www.octolabs.com/so-auth 视频链接在这里,讲解了所有内容:http://www.youtube.com/watch?v=L1B_HpCW8bs 这种集中式单点登录系统并不简单,但 Jeremy 在讨论服务导向架构和分享如何构建此系统方面做得非常好。

3

我最近遇到了一个类似的问题,想要在Rails和Erlang应用程序之间共享会话数据。我的解决方案是编写一个Rack::Session::Abstract::ID类,将会话存储在Redis中作为哈希值。它不会对String类型调用Marshal.dump。这允许非Ruby应用程序在具备session_id的情况下使用一些会话值。

require 'rack/session/abstract/id'

class MaybeMarshalRedisSession < Rack::Session::Abstract::ID

  def initialize(app, options = {})
    @redis  = options.delete(:redis) || Redis.current
    @expiry = options[:expire_after] ||= (60 * 60 * 24)
    @prefix = options[:key] || 'rack.session'
    @session_key = "#{@prefix}:%s"
    super
  end

  def get_session(env, sid)
    sid ||= generate_sid
    session = @redis.hgetall(@session_key % sid)
    session.each_pair do |key, value|
      session[key] = begin
        Marshal.load(value)
      rescue TypeError
        value
      end
    end

    [sid, session]
  end

  def set_session(env, sid, session, options={})
    @redis.multi do
      session.each_pair do |key, value|
        # keep string values bare so other languages can read them
        value = value.is_a?(String) ? value : Marshal.dump(value)
        @redis.hset(@session_key % sid, key, value)
      end
      @redis.expire(@session_key % sid, @expiry)
    end

    sid
  end

  def destroy_session(env, sid, option={})
    @redis.del(@session_key % sid)
    generate_sid unless options[:drop]
  end

end

你可以在 Rails 中使用以下代码:

 MyApp::Application.config.session_store MaybeMarshalRedisSession

从机架中获取:

 use MaybeMarshalRedisSession

而来自其他地方的是:

redis.hgetall("rack.session:#{session_id}")

如果您想从您的MainApp或Node.js中调用PhotoApp,您可以发起一个HTTP请求,并包含用户会话cookie。

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