铸造坊 - 集成 OpenID

作者:Reuven M. Lerner

在过去的几个月里,我们研究了两种不同的方法来验证访问网站的用户的身份。首先,我们研究了 OpenID,这是一种日益普及的分布式身份验证系统。通过 OpenID,用户可以控制自己的信息,以及哪些应用程序被允许使用这些信息。

上个月,我们研究了 acts_as_authenticated,这是一个 Ruby on Rails 框架的插件,它非常传统,要求访问者输入用户名和密码才能访问受限服务。

本月,我们将初步了解如何将 OpenID——以及 OpenID 和传统身份验证的组合——整合到我们自己的 Rails 应用程序中。在 OpenID 术语中,我们希望我们的应用程序成为“消费者”,向用户选择的 OpenID “提供商”索取身份验证信息,而不是自己收集和检查该信息。

OpenID 是一个相当成熟的标准,集成到 Rails 应用程序中并不那么困难。然而,支持 OpenID 的库和插件的数量已经有点失控,以至于有时很难知道(或相信)哪些库和插件实际上可以工作,更不用说哪些库和插件最容易使用了。

身份验证和 OpenID

为网站验证用户身份通常是一项简单的任务。您通过 HTML 表单要求用户输入用户名和密码,然后将该组合与数据库进行比较。(当然,出于安全目的,通常最好在数据库中加密密码,然后将加密后的输入与数据库中的内容进行比较。)如果数据库中存在用户名/密码组合,则用户可以登录。

当然,HTTP 是一种无状态协议,这意味着实际上不存在“已登录”这种状态。相反,我们依赖 cookie,即由服务器提供但在用户浏览器中存储的数据片段,这些数据片段在随后的每个 HTTP 请求中都会传递到服务器。在这个系统中,当服务器在用户的浏览器上设置 cookie 时,登录就会发生。在 Rails 和许多其他 Web 框架中,cookie 也用于跟踪用户的“会话”,即与此浏览器上的此用户关联的属性。

为了将 OpenID 整合到 Web 应用程序中,我们不需要替换框架的整个 cookie/会话/登录部分。相反,我们需要更改验证用户身份的方式,在 OpenID 提供商表明用户已合法识别后设置登录 cookie。

传统的基于 Rails 的登录系统将涉及 HTML 表单、将提交的表单值与数据库进行比较的控制器操作,以及登录页面。为了用 OpenID 替换它,我们需要修改我们的控制器,使其请求 OpenID 服务器来验证用户身份。

但是,等一下。OpenID 的重点是用户输入 URL(即他们唯一的 OpenID),并且他们针对与该 URL 关联的服务器进行身份验证。这意味着 HTML 表单需要更改,使其请求 URL 而不是用户名和密码。

此外,我们必须考虑到这样一个事实,即我们的服务器需要将用户重定向到 OpenID 服务器,然后 OpenID 服务器将重定向回我们的系统,指示用户是否已成功登录。

正如我在上面指出的,有许多与 Ruby 和 Rails 相关的资源与 OpenID 有关。不幸的是,其中许多资源文档不完善、过时或相对难以使用。例如,有一个名为 openid_login 的 Ruby gem 和一个名为 open_id_authentication 的插件,它们可能可以通过一些修改来工作。但是,它们的文档已经过时,并且我遇到了问题,其中包括 Rails 现在在模板中使用的双后缀 (.html.erb)。因此,虽然我确信有可能使这个 gem 与 OpenID 和现代 Rails 安装一起工作,但这可能需要时间和精力——比我期望从预打包解决方案中获得的更多。

因此,我对整个 OpenID 问题的建议解决方案是使用简单、低级的 ruby-openid gem,它恰好内置了对 Rails 应用程序的支持。这个 gem 在其当前形式(撰写本文时为 2.0.4 版本)中实际上有非常完善的文档。但是,请注意;您在网上找到的大部分文档都已过时,并且使用此 gem 的 1.x 版本和较旧的、不兼容的 API 实现了与 OpenID 相关的功能。

要安装 gem,当然,我们写

gem install ruby-openid

然后我们创建一个控制器来处理我们与 OpenID 相关的操作

script/generate controller openid new create complete openid_consumer

这四个操作(其中第四个是私有的)是我们让人们使用 OpenID 登录所需的。

现在我们可以在视图中创建一个 HTML 表单;我在 views/openid/new.html.erb 中创建了这个简单的视图作为 login.html.erb

<html>
<head>
    <title>Log in with OpenID</title>
</head>
<body>
    <% if not flash[:error].blank? %>
        <p><b><%= flash[:error] -%></b></p>
    <% end %>

    <% form_tag "/openid/create" do %>
      <%= text_field_tag "openid_url" %>
      <%= submit_tag "Log in with OpenID" %>
    <% end %>
</body>
</html>

由于 ERb 模板中 <% 和 %> 之间的所有内容都作为 Ruby 代码进行评估,因此我们需要了解这里发生了什么。首先,我们使用 form_tag 助手创建一个未连接到任何对象的表单。(如果表单连接到对象,我们将只使用 form 助手。)我们给它一个 /openid 的 URL,当我们查看路由时,我们将在稍后讨论它。

表单包含一个文本字段,其 name 和 id 属性都将设置为 openid_url。现代浏览器识别此名称并使用它来自动填写 OpenID URL。提交按钮和结束标记完成了表单。

存储用户信息

当我们在浏览器中显示此表单时,用户只有一个选项——即通过输入 URL 使用 OpenID 登录。调用的操作 (create) 必须找到用户的 OpenID 服务器并重定向到该服务器。为了做到这一点,我们需要 OpenID::Consumer 的实例,这是一个由 ruby-openid gem 定义的对象。因为我们将继续需要它,所以我们可以将其创建为实例变量

def openid_consumer
 if @openid_consumer.blank?
   @openid_consumer =
     OpenID::Consumer.new(session,
           OpenID::Store::Filesystem.new("#{RAILS_ROOT}/tmp/openid"))
 end

 return @openid_consumer
end

请注意,我们将 OpenID 信息存储在文件系统上,即 Rails 项目目录根目录下的 tmp 目录中。当您有多个 Web 服务器时,这是一个坏主意,但对于小型或初创网站来说,这当然足够好了。

现在我们有了一个名为 openid_consumer 的方法和一个名为 @openid_consumer 的实例变量,我们可以实现 create 操作,我们的 HTML 表单将提交到该操作

def create
 # Get the OpenID parameter
 openid_url = params[:openid_url]

 # Make sure we got something
 if openid_url.blank?
   flash[:error] = "No OpenID was entered; try again"
   redirect_to :back
   return
 end

 # Get an OpenID response
 openid_response = openid_consumer.begin openid_url

 home_url = url_for :controller => "openid", :action => "index"
 complete_url = url_for :controller => "openid", :action => "complete"
 openid_redirect_url = openid_response.redirect_url(home_url, complete_url)
 redirect_to openid_redirect_url

 return
end

换句话说,我们获取用户的 OpenID URL,并检查它是否为空。然后,我们使用我们的 OpenID::Consumer 实例开始 OpenID 登录过程,使用 open_consumer.begin,并将用户的 OpenID URL 传递给它。如果一切顺利,这将返回 SuccessRequest 的实例,它还向我们提供了我们应该将用户重定向到的 URL。(如果请求失败,响应将是 OpenIDStatus 的子类。)

完成登录过程

当我们将用户发送到用户的 OpenID 服务器时,我们必须提供两个不同的 URL 作为参数:一个我们称之为 home_url,另一个我们称之为 complete_url。前者是我们网站的根 URL;通常,它将是一个顶级 URL。后者 complete_url 告诉 OpenID 服务器在用户登录后应将用户重定向到哪个 URL。在这两种情况下,我都使用了内置的 Rails url_for 方法,该方法从控制器和操作名称构造 URL。

当用户从 OpenID 服务器返回时,它将返回到 complete_url 中指示的 URL。这意味着我们还必须定义我们的 complete 方法

def complete
 home_url = url_for :controller => "openid", :action => "index"
 complete_url = url_for :controller => "openid", :action => "complete"

 openid_response = openid_consumer.complete(params, complete_url)

 session[:openid] = openid_response.identity_url
 flash[:error] = "You have been logged in as '#{session[:openid]}'"
 redirect_to :action => "new"
 return
end

再次定义 home_url 和 complete_url 后,我们在 OpenID::Consumer 的实例上调用 complete 方法。如果响应良好(这里我们假设是这样,忽略了我们可能收到了 OpenIDStatus 实例的可能性)。显然,您的实际应用程序应包含此类检查。

果然,当我们把这一切都就位时,它就可以工作了!我们可以将我们的用户 ID 输入到 HTML 表单中。我们得到了用户 OpenID 服务器的验证,即使这意味着另一次重定向。并且,我们获得了经过基本信息验证的用户。

列表 1. openid_controller.rb

require 'openid'
require 'openid/store/filesystem'

class OpenidController < ApplicationController

 def openid_consumer
  if @openid_consumer.blank?
    @openid_consumer =
      OpenID::Consumer.new(session,
         OpenID::Store::Filesystem.new("#{RAILS_ROOT}/tmp/openid"))
  end

  return @openid_consumer
 end

 def new
  # Nothing to do here -- it's all in the form
 end

 def create
  # Get the OpenID parameter
  openid_url = params[:openid_url]

  # Make sure we got something
  if openid_url.blank?
    flash[:error] = "No OpenID was entered; try again"
    redirect_to :back
    return
  end

# Get an OpenID response
  openid_response = openid_consumer.begin openid_url

  home_url = url_for :controller => "openid", :action => "index"
  complete_url = url_for :controller => "openid", :action => "complete"
  openid_redirect_url = openid_response.redirect_url(home_url, complete_url)
  redirect_to openid_redirect_url

  return
 end

 def complete
  home_url = url_for :controller => "openid", :action => "index"
  complete_url = url_for :controller => "openid", :action => "complete"

  openid_response = openid_consumer.complete(params, complete_url)

  session[:openid] = openid_response.identity_url
  flash[:error] = "You have been logged in as '#{session[:openid]}'"
  redirect_to :action => "new"
  return
 end

 def clear_session
  reset_session
  flash[:error] = "Session cleared."
  redirect_to :action => "new"
 end

end

结论

OpenID 是一个简单但强大的理念,它正在缓慢但肯定地改变我们管理互联网身份的方式。越来越多的应用程序使用 OpenID,它在用户中也变得越来越流行。

向应用程序添加 OpenID 不需要复杂或困难。正如我本月所示,将 OpenID 整合到 Rails 应用程序中需要理解一个特定的 Ruby 对象,即 OpenID::Consumer,以及奇怪的、基于重定向的三部分 OpenID 登录系统规范。

资源

OpenID:OpenID 的主页是 openid.net。有关 OpenID 的 Ruby gem 的文档,请参阅 openidenabled.com/files/ruby-openid/docs/2.0.4/classes/OpenID/Consumer.html

Rails 上的 OpenID:此功能的主 Wiki 页面是 wiki.rubyonrails.org/rails/pages/OpenID

有很多关于 OpenID 和 Rails 的博客文章和教程,其中一些比另一些更过时。也许最好的是 railscasts.com/episodes/68,这是一个关于正在发生的事情的很好的可视化介绍(以及源代码)。

Reuven M. Lerner,一位资深的 Web/数据库开发人员和顾问,是西北大学学习科学博士候选人,研究在线学习社区。在芝加哥地区生活四年后,他最近(与妻子和三个孩子)返回他们在以色列莫迪因的家。

加载 Disqus 评论