User Group List and Information (UGLI) 应用程序已开始成形。您现在可通过在 “使用 MEAN 和 UGLI CRUD 实现响应式 Web 设计” 中设置的 CRUD 屏幕,展示您创建的本地内容。您也可以使用 “当 MEAN 遇到 Meetup.com 和微数据” 中开发的服务来合并来自外部站点的内容。

与一般大众分享会议信息,是此项目的一个重要部分。但作为一位用户组主管,我还想将一些活动限制到该组的注册成员范围内。例如,我可选择通过关闭匿名访问和要求登录,让对我们的演示文稿的评论保持一定的文明态度。所以在这一期的文章中,您将使用的 Meetup.com 的 OAuth 服务向 UGLI 应用程序提供登录功能。请参见 下载,获取示例代码。

创建一个新用户帐户

通过单击 Sign up 按钮(如图 1 所示),您应用程序的用户可创建一个存储在 MongoDB 本地的新帐户。此功能构建于您的应用程序中 — 不需要额外的编程。

图 1. UGLI 注册页面

UGLI 注册页面的屏幕截图

关于本系列

在使用开源软件构建专业网站方面,新兴的 MEANMongoDBExpressAngularJSNode.js)堆栈对一直以来广受欢迎的 LAMP 堆栈形成了巨大冲击。MEAN 代表架构和思维模型的一次重要转变 — 从关系数据库转变为 NoSQL,以及从服务器端 Model-View-Controller 转变为客户端单页面应用程序。在此文章系列中,我们将了解 MEAN 堆栈技术如何相互补充,以及如何使用它创建面向 21 世纪的现代化的全堆栈 JavaScript Web 应用程序。

这种默认行为显然从开发角度讲是最简单的解决方案,但它从用户体验角度将它还需要一些东西。您的用户组成员已在 Meetup.com 上拥有一个帐户,他们使用该帐户来对即将举办的会议执行 RSVP。要求他们创建和维护一个重复的凭据集合不仅让人厌烦 — 还公然违反了 “不要重复自己 (DRY) “ 的原则。

身份验证与授权

只要登录到一个网站,就会发生两件稍微不同但相关的事情:

  • 通过提供一个用户名和密码(或一个指纹,或视网膜扫描,或……),您就可以对自己进行身份验证— 通过共享一个秘密密码或一个惟一标识符,证明您就是您声称的那个人。没有身份验证,任何人都可登录到您的银行网站取钱,或者登录到您的社交媒体帐户,在您的游戏中发表煽动性言论。
  • 经过身份验证后,系统会确定您有权 执行哪些操作。您的银行网站授权您在您的帐户中存钱和取钱,但很可能它不允许您调整利率或撤销银行手续费。要执行这些类型的操作,您必须联系一位客户服务代表,这位代表的帐户要有权执行管理任务。

幸运的是,借助您使用的 MEAN 堆栈,您可以设置一个使用 OAuth 和 Passport 的分布式身份验证和授权。简单地讲,您的用户可使用他们登录到 Meetup.com 所用的凭据来登录(执行验证)到 UGLI 应用程序。但没有用户的权限,就无法实现此目的;他们必须允许(授权)UGLI 应用程序使用其 Meetup.com 凭据。

在 UGLI 应用程序通过 OAuth 授权后,用户凭据不会与授权的应用程序共享。您不会在 UGLI 本地存储一组重复的用户名和密码。希望被授权 (UGLI) 的应用程序将用户重定向到 OAuth 提供商 (Meetup.com),他们在这里提供其凭据(用户名和密码)。用户成功执行身份验证后,会将一个访问令牌返回给授权的应用程序。

此模式在应用程序中消除了大量代码和逻辑。您不必再担心会将加密的密码存储在您的服务器上 — 这现在是 OAuth 提供商要处理的问题。类似地,您不必在编写算法来执行强密码,或者处理忘记密码的问题,或者强制用户定期更改其密码。

所以,OAuth 为用户提供了更少的要记住的密码,显著减少了您要编写的代码。如果这还不是一种双赢方案的教科书式定义,我不知道该怎么称呼它了。

回页首

介绍 OAuth 和 Passport

OAuth 是一种分布式身份验证和授权的开放标准。它于 2006 年由 Twitter 和业务合作伙伴 Ma.gnolia 开发,用来方便地创建一些桌面小部件,这些小部件可以显示来自已身份验证服务的信息。从那时起,数百家大型网站采用了 OAuth,这些网站包括 Google、Facebook、Twitter、GitHub、LinkedIn 等。(参见 著名 OAuth 服务提供商名单。)

Passport 是一个为 Node.js 编写的 OAuth 库。具体地讲,它是一个中间件,用于与 Express 应用程序无缝地合并在一起。有 140 多个 Passport 插件(称为策略)可用,它们是为每个 OAuth 提供商而量身订造。

如果在文本编辑器中打开 UGLI 应用程序的 package.json 文件(如清单 1 所示),您可看到 4 个主要服务商的 Passport 策略(Facebook、Twitter、LinkedIn 和 Google),以及一个用于直接在 MongoDB 中存储凭据的本地策略。

清单 1. package.json 中的 Passport 策略
"dependencies": {
    "passport": "~0.2.0",
    "passport-local": "~1.0.0",
    "passport-facebook": "~1.0.2",
    "passport-twitter": "~1.0.2",
    "passport-linkedin": "~0.1.3",
    "passport-google-oauth": "~0.1.5"
}

使用现有的示例作为指南,您将添加第六个策略来整合 Meetup.com,作为 UGLI 的 OAuth 提供商。

回页首

安装 Meetup.com Passport 策略

如果访问 使用 Meetup API 执行身份验证,您将看到 Meetup.com 提供了 OAuth 服务。在网上快速搜索 meetup.com passport.js strategy 就会得到您寻找的库的链接:passport-meetup

键入 npm install passport-meetup --save 来将该库下载到 node_modules,并更新 package.json 中的依赖关系代码块。

这是最简单的部分。有了该策略,下一步是将它合并到注册和登录阶段中。

回页首

将一个 Meetup.com 链接添加到注册和登录阶段中

在文本编辑器中打开 public/modules/users/views/signup.client.view.html。在该文件顶部,您可看到各种 OAuth 提供商的链接(如清单 2 所示)。

清单 2. public/modules/users/views/signup.client.view.html
<h3 class="col-md-12 text-center">Sign up using your social accounts</h3>
<div class="col-md-12 text-center">
    <a href="/auth/facebook" class="undecorated-link">
        <img src="/modules/users/img/buttons/facebook.png">
    </a>
    <a href="/auth/twitter" class="undecorated-link">
        <img src="/modules/users/img/buttons/twitter.png">
    </a>
    <a href="/auth/google" class="undecorated-link">
        <img src="/modules/users/img/buttons/google.png">
    </a>
    <a href="/auth/linkedin" class="undecorated-link">
        <img src="/modules/users/img/buttons/linkedin.png">
    </a>
</div>

将现有的链接替换为一个指向要创建的 /auth/meetup 路由,并显示为 Meetup.com 图标的链接(如清单 3 所示)。

清单 3. auth/meetup 的链接
<h3 class="col-md-12 text-center">Sign up using your Meetup.com account</h3>
<div class="col-md-12 text-center">
    <a href="/auth/meetup" class="undecorated-link">
        <img src="/modules/users/img/buttons/meetup.png">
    </a>
</div>

访问 Meetup Icon 页面,将这个 128x128 像素的图像保存到 public/modules/users/img/buttons/ 中,其他社交媒体图标也存储在这里。

现在,注册页面存根的创建已完成,在文本编辑器中打开 public/modules/users/views/signin.client.view.html,对注册页面采取的相同方式来调整它(如清单 4 所示)。

清单 4. public/modules/users/views/signin.client.view.html
<h3 class="col-md-12 text-center">Sign in using your Meetup.com account</h3>
<div class="col-md-12 text-center">
    <a href="/auth/meetup" class="undecorated-link">
        <img src="/modules/users/img/buttons/meetup.png">
    </a>
    </div>

如果一切按计划进行,那么您的新注册页面将类似于图 2。当然,如果没有路由,单击该链接时就会获得一个404 Page Not Found 错误。接下来修复此错误。

图 2. 新的 UGLI 注册页面

新 UGLI 注册页面的屏幕截图

回页首

设置服务器端 auth/meetup 路由

下一步是创建服务器端 auth/meetup 路由。回想一下,所有服务器端逻辑都存储在应用程序目录中;客户端逻辑存储在 public 文件夹中。

在文本编辑器中打开 app/routes/users.server.routes.js。找到 Facebook 的代码块,复制/粘贴它,将 facebook 替换为 meetup(如清单 5 所示)。

清单 5. app/routes/users.server.routes.js
// Setting the facebook oauth routes
app.route('/auth/facebook').get(passport.authenticate('facebook', {
    scope: ['email']
}));
app.route('/auth/facebook/callback').get(users.oauthCallback('facebook'));

// Setting the meetup oauth routes
app.route('/auth/meetup').get(passport.authenticate('meetup', {
    scope: ['email']
}));
app.route('/auth/meetup/callback').get(users.oauthCallback('meetup'));

还记得您在上一节中在注册和登录页面上创建的 auth/meetup 的超链接吗?第一个路由 (auth/meetup) 将在用户单击该链接来向服务器发送一个 HTTP GET 请求时触发。Passport 将尝试使用 passport-meetup 策略来验证用户。登录尝试的结果(成功与否)将异步地发送给第二个 auth/meetup/callback 路由。

如果现在单击注册页面上的 Meetup 链接,您会获得一个 500 Server Error,而不是 404。准确地讲,这不是一项改进,但至少会有所进步。下一步:配置 Meetup 策略。

回页首

配置 Meetup 策略

可以在著名的 config/strategies 目录中找到所有 Passport 策略。将 facebook.js 复制为 meetup.js,然后在文本编辑器中打开 meetup.js。

就像上一节中一样,您将浏览此文件,将所有 facebook 实例替换为 meetup。但这不是一个简单的查找/替换操作。还需要执行一些细微的配置更改。

首先,将文件顶部的必需的库从 Facebook 策略更改为 Meetup 策略(如清单 6 所示)。

清单 6. config/strategies/meetup.js
/**
 * Module dependencies.
 */
var passport = require('passport'),
    url = require('url'),
    MeetupStrategy = require('passport-meetup').Strategy,
    config = require('../config'),
    users = require('../../app/controllers/users');

接下来,需要自定义传入到新策略中的选项块。这些值在各个策略之间有所不同。清单 7 给出了 Facebook 策略选项,这些选项不适合 Meetup。

清单 7. 不适合 Meetup 的 Facebook 选项
module.exports = function() {
    // Use facebook strategy
    passport.use(new FacebookStrategy({
            clientID: config.facebook.clientID,
            clientSecret: config.facebook.clientSecret,
            callbackURL: config.facebook.callbackURL,
            passReqToCallback: true
        },

幸运的是,您之前执行 npm install 的 passport-meetup 模块随带了示例代码。在文本编辑器中打开 node_modules/passport-meetup/examples/login/app.js。查找 passport.use 函数调用(如清单 8 所示)。

清单 8. node_modules/passport-meetup/examples/login/app.js
passport.use(new MeetupStrategy({
    consumerKey: MEETUP_KEY,
    consumerSecret: MEETUP_SECRET,
    callbackURL: "http://127.0.0.1:3000/auth/meetup/callback"
  },

将这段代码复制到 meetup.js,覆盖 Facebook 代码。接下来,将分号右侧的值更改为清单 9 中所示的值。

清单 9. 将有效的 Meetup 选项
passport.use(new MeetupStrategy({
    consumerKey: config.meetup.consumerKey,
    consumerSecret: config.meetup.consumerSecret,
    callbackURL: config.meetup.callbackURL,
    },

OAuth 访问令牌和刷新令牌

访问令牌就像一张电影片:您可从售票处获得一张小纸片,证明您已付费获得了进入观看的权利。进入电影院时,您将这个纸片提供给一位员工,他告诉您您有权看哪场电影 — 并将这个纸片撕掉一半,确保它仅能使用一次。

类似地,OAuth 访问令牌是一个具有较短存活时间 (TTL) 的一次性令牌。下一次用户通过 Meetup 链接登录到 UGLI 时,他或她不需要提供用户名和密码。Passport 会向 Meetup 发送一个刷新令牌。除非用户撤销从 Meetup 对 UGLI 的访问,否则 Meetup 会以一个具有较短 TTL 的新访问令牌作为响应。

刷新令牌应在系统中存在较长时间 — 基本上讲,只要用户帐户存在,它就会存在。相对而言,访问令牌是寿命较短的暂时性对象,每次用户向远程 OAuth 提供商执行身份验证时都会重新生成它。

在下一节中,您将从 Meetup.com 获取 consumerKey 和 consumerSecret,并将它们保存在 config.js 文件中。但您在继续后面的操作之前,需要对当前的文件执行两处额外的更改。

紧随新 MeetupStrategy 构造函数之后的函数是从 Meetup.com 接收响应的事件处理函数。我们对响应的 3 个重要部分感兴趣:访问令牌、刷新令牌和用户配置文件。(参见 OAuth 访问令牌和刷新令牌 边栏来获取这些令牌的详细信息。)

访问令牌和刷新令牌是您原封不动地传递给 Passport 的字符串。尽管它们对 OAuth 操作的成功至关重要,但就它们本身而言看起来很令人讨厌。(清单 10 包含二者的示例。)

用户配置文件更有趣。它是 OAuth 提供商返回的一个 JSON 对象,其中包含成功经过身份验证的用户的信息。不同 OAuth 提供商之间的具体细节不同。清单 10 给出了 Meetup 返回的用户配置文件的一个示例。

清单 10. 从 Meetup.com OAuth 提供商返回的用户配置文件
{ 
  provider: 'meetup',
  id: 13848777,
  displayName: 'Scott Davis',
  _raw: '{ 
      "results": [{
        "status": "active",
        "link": "http:\\\/\\\/www.meetup.com\\\/members\\\/13848777",
        "photo": {
          "photo_link": "http:\\\/\\\/photos1.meetupstatic.com\\\/photos\\\/member\\\/7\\\/4\\\/d\\\/2\\
\/member_11849906.jpeg",
          "thumb_link": "http:\\\/\\\/photos3.meetupstatic.com\\\/photos\\\/member\\\/7\\\/4\\\/d\\\/2\\
\/thumb_11849906.jpeg",
          "photo_id": 11849906
        },
        "country": "us",
        "state": "CO",        
        "city": "Denver",
        "id": 13848777,
        "joined": 1295844957000,
        "bio": "Scott Davis is the founder of ThirstyHead.com, a training and 
consulting company that specializes in leading-edge technology solutions like 
HTML 5, NoSQL, Groovy, and Grails.",
        "name": "Scott Davis",
        "other_services": {
          "twitter": {
            "identifier": "@scottdavis99"
          }
        }
      }]  
   }',
  _json: { 
     results: [ [Object] ],
     meta: { 
        link: 'https://api.meetup.com/2/members',
        total_count: 1,
        url: 'https://api.meetup.com/2/members?order=name&member_id=13848777&offset=0
&format=json&page=800',
        title: 'Meetup Members v2',
        updated: 1392763702000,
        description: 'API method for accessing members of Meetup Groups',
        method: 'Members',
     },
     accessToken: 'c7b5577bb80aab55439785cd86abcdef',
     refreshToken: '2af98db68950235a1e2519a734abcdef' 
  } 
}

可以看到,Meetup 返回用户详细信息,比如姓名、住址、加入日期、面部照片、链接的社交媒体帐户等。

需要做的最后一件事是完成 Meetup 策略的自定义,将 Meetup 配置文件字段映射回 app/models/user.server.model.js 中定义的 User Mongoose 对象。编辑 config/strategies/meetup.js 中剩余的 Facebook 部分,如清单 11 所示。

清单 11. 将 OAuth 用户配置文件映射到 User 对象
// Create the user OAuth profile
var providerUserProfile = {
    firstName: '',
    lastName: '',
    displayName: profile.displayName,
    email: '',
    username: profile.id,
    provider: profile.provider,
    providerIdentifierField: 'id',
    providerData: providerData
};

如果在 Meetup 配置文件中看到您想要添加到 User 对象中的 JSON,这是执行更改的完美时机。不要忘记向 public/modules/users/views 中的 HTML 表单添加新字段。

您完成的 config/strategies/meetup.js 应类似于清单 12。

清单 12. 完整的 config/strategies/meetup.js
'use strict';

/**
 * Module dependencies.
 */
var passport = require('passport'),
    url = require('url'),
    MeetupStrategy = require('passport-meetup').Strategy,
    config = require('../config'),
    users = require('../../app/controllers/users');

module.exports = function() {
    // Use meetup strategy
    passport.use(new MeetupStrategy({
        consumerKey: config.meetup.clientID,
        consumerSecret: config.meetup.clientSecret,
        callbackURL: config.meetup.callbackURL,
        },
        function(req, accessToken, refreshToken, profile, done) {
            // Set the provider data and include tokens

            var providerData = profile._json;
            providerData.accessToken = accessToken;
            providerData.refreshToken = refreshToken;

            // Create the user OAuth profile
            var providerUserProfile = {
                firstName: '',
                lastName: '',
                displayName: profile.displayName,
                email: '',
                username: profile.id,
                provider: profile.provider,
                providerIdentifierField: 'id',
                providerData: providerData
            };

            // Save the user OAuth profile
            users.saveOAuthUserProfile(req, providerUserProfile, done);
        }
    ));
};

测试此代码之前,需要执行另外一件事:从 Meetup 获取 consumerKey 和 consumerSecret。

回页首

获取 Meetup consumerKey 和 consumerSecret

目前为止,我用了整篇文章来讨论用户的身份验证。但在用户使用 OAuth 登录到 UGLI 之前,您(开发人员)必须提供证据证明您的组织就是它所声称的实体。为此,将一个公钥 (consumerKey) 提供给用户。您的应用程序还需要知道它的私钥 (consumerSecret) 是什么。

如果之前使用过 公钥基础架构 (PKI),您就会知道保持私钥隐藏和安全至关重要。如果其他某人发现了您的私钥,他们就可伪装成您的组织。相反,如果您不与用户共享您的公钥,他们就无法证明您是谁。

如果您是 Meetup.com 上一个用户组的组织者,那么您可以从 Meetup 的 Your OAuth Consumers 页面上生成 consumerKey 和 consumerSecret(参见图 3)。我使用了 HTML5 Denver User Group 作为 Consumer Name,使用 http://www.meetup.com/HTML5-Denver-Users-Group/ 作为 Application Website,使用 http://localhost:3000/auth/meetup/callback 作为 Redirect URI。在我将 UGLI 应用程序推送到生产环境中后,我会将 Application Website 更改为 http://html5denver.com,将 Redirect URI 更改为 http://html5denver.com/auth/meetup/callback。

图 3. 为 HTML5 Denver 生成 consumerKey 和 consumerSecret

为 HTML5 Denver 生成 consumerKey 和 consumerSecret 的屏幕截图

点击查看大图

如果不在 Meetup.com 上运行用户组,那么您的帐户将无权代表一个组来生成 OAuth 密钥。但是,您仍然可以为您的其他某个社交媒体帐户生成令牌,并相应地调整本文中的步骤。参见 使用 Twitter 实现登录 来生成您的 Twitter 帐户的应用程序密钥,或者参见 访问令牌 来使用您的 Facebook 帐户。在网络上快速搜索 _您的社交媒体网站_oauth 密钥,应该会找到分步操作说明。

回页首

将组织 OAuth 密钥添加到您应用程序中

拥有两个密钥(公钥和私钥)之后,可将通过环境变量将它们添加到您的应用程序中 — 非常类似于 “了解一个 MEAN 应用程序” 中更改 PORT 的方式。

回想一下,您可设置会基于您的运行模式而更改的变量:development、production 或 test。特定于环境的值存储在 config/env 中。在文本编辑器中打开 config/env/development.js。复制/粘贴 Facebook 代码块,针对 Meetup 而相应地调整它(如清单 13 所示)。确保这里的属性名称与您在 config/strategies/meetup.js 中的 passport.use 函数调用中使用的属性名称相同。

清单 13. config/env/development.js
'use strict';

module.exports = {
    db: 'mongodb://localhost/test-dev',
    app: {
        title: 'HTML5 Denver'
    },

    meetup: {
        consumerKey: process.env.MEETUP_KEY || 'APP_ID',
        consumerSecret: process.env.MEETUP_SECRET || 'APP_SECRET',
        callbackURL: 'http://localhost:3000/auth/meetup/callback'
    },    
    facebook: {
        clientID: process.env.FACEBOOK_ID || 'APP_ID',
        clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET',
        callbackURL: 'http://localhost:3000/auth/facebook/callback'
    },
    twitter: {
        clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY',
        clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET',
        callbackURL: 'http://localhost:3000/auth/twitter/callback'
    },
    google: {
        clientID: process.env.GOOGLE_ID || 'APP_ID',
        clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET',
        callbackURL: 'http://localhost:3000/auth/google/callback'
    },
    linkedin: {
        clientID: process.env.LINKEDIN_ID || 'APP_ID',
        clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET',
        callbackURL: 'http://localhost:3000/auth/linkedin/callback'
    }
};

可将 APP_ID 和 APP_SECRET 替换为您在上一节中获取的硬编码的 consumerKey 和 consumerSecret 值。但一种更安全的解决方案是,通过环境变量将这些值提供给 UGLI 应用程序。要使用您组织的 consumerKey 和 consumerSecret 来启动您的应用程序,可键入:

MEETUP_KEY=l75fkklhurkack36eelfhhfhjc MEETUP_SECRET=abcdeg316jd3ni43f21u1abcde NODE_ENV=development grunt

在上线之前不要忘记对 config/env/production.js 执行类似的调整。如果之前创建了一个用户帐户,一定要从 MongoDB 中的 html5-denver-dev 数据库中删除它,以便可以再次执行新帐户创建过程。

回页首

Larry Wall(Perl 编程语言的创建者)有句至理名言,“容易的事应该是简单的事,艰难的事应该是可能的事。” 我希望这种态度很好地总结了您连接 OAuth 和 Passport,使用 Meetup.com 满足您的分布式身份验证和授权需要的体验。

在下一期精通 MEAN 中,我将介绍构建于 MEAN 堆栈中的测试基础架构。您将学习使用 Mocha 执行服务器端测试,使用 Jasmine 执行客户端测试,还将学习使用 Karma 跨多个浏览器运行您的测试。请届时继续阅读 “精通 MEAN”。

回页首

<table><thead><tr>描述名字大小</tr></thead><tbody><tr>示例代码<td>wa-mean5.zip</td><td>1.4MB</td></tr></tbody></table>