铸造坊 - 集成 OpenID
在过去的几个月里,我们研究了两种不同的方法来验证访问网站的用户的身份。首先,我们研究了 OpenID,这是一种日益普及的分布式身份验证系统。通过 OpenID,用户可以控制自己的信息,以及哪些应用程序被允许使用这些信息。
上个月,我们研究了 acts_as_authenticated,这是一个 Ruby on Rails 框架的插件,它非常传统,要求访问者输入用户名和密码才能访问受限服务。
本月,我们将初步了解如何将 OpenID——以及 OpenID 和传统身份验证的组合——整合到我们自己的 Rails 应用程序中。在 OpenID 术语中,我们希望我们的应用程序成为“消费者”,向用户选择的 OpenID “提供商”索取身份验证信息,而不是自己收集和检查该信息。
OpenID 是一个相当成熟的标准,集成到 Rails 应用程序中并不那么困难。然而,支持 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/数据库开发人员和顾问,是西北大学学习科学博士候选人,研究在线学习社区。在芝加哥地区生活四年后,他最近(与妻子和三个孩子)返回他们在以色列莫迪因的家。