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

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

## 创建一个新用户帐户

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

##### 图 1. UGLI 注册页面

![UGLI 注册页面的屏幕截图][3]

## 关于本系列

在使用开源软件构建专业网站方面,新兴的 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 为用户提供了更少的要记住的密码,显著减少了您要编写的代码。如果这还不是一种双赢方案的教科书式定义,我不知道该怎么称呼它了。

[回页首][4]

## 介绍 OAuth 和 Passport

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

[Passport][6] 是一个为 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 提供商。

[回页首][4]

## 安装 Meetup.com Passport 策略

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

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

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

[回页首][4]

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

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

##### 清单 2. public/modules/users/views/signup.client.view.html

Sign up using your social accounts

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

##### 清单 3. auth/meetup 的链接

Sign up using your Meetup.com account

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

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

##### 清单 4. public/modules/users/views/signin.client.view.html

Sign in using your Meetup.com account

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

##### 图 2. 新的 UGLI 注册页面

![新 UGLI 注册页面的屏幕截图][10]

[回页首][4]

## 设置服务器端 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 策略。

[回页首][4]

## 配置 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 访问令牌和刷新令牌][11] 边栏来获取这些令牌的详细信息。)

访问令牌和刷新令牌是您原封不动地传递给 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。

[回页首][4]

## 获取 Meetup consumerKey 和 consumerSecret

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

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

如果您是 Meetup.com 上一个用户组的组织者,那么您可以从 Meetup 的 [Your OAuth Consumers][13] 页面上生成 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 的屏幕截图][14]

[点击查看大图][15]

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

[回页首][4]

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

拥有两个密钥(公钥和私钥)之后,可将通过环境变量将它们添加到您的应用程序中 — 非常类似于 “[了解一个 MEAN 应用程序][18]” 中更改 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 数据库中删除它,以便可以再次执行新帐户创建过程。

[回页首][4]

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

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

[回页首][4]

描述名字大小示例代码

[wa-mean5.zip][19] 1.4MB

[0]: http://www.ibm.com/developerworks/cn/web/wa-mean3/index.html
[1]: http://www.ibm.com/developerworks/cn/web/wa-mean4/index.html
[2]: http://www.ibm.com/developerworks/cn/web/wa-mean5/index.html#download
[3]: http://www.ibm.com/developerworks/cn/web/wa-mean5/signup.jpg
[4]: http://www.ibm.com/developerworks/cn/web/wa-mean5/index.html#ibm-pcon
[5]: http://en.wikipedia.org/wiki/OAuth#List_of_notable_OAuth_service_providers
[6]: http://passportjs.org/
[7]: http://www.meetup.com/meetup_api/auth/
[8]: https://github.com/jaredhanson/passport-meetup
[9]: http://www.iconarchive.com/show/social-2-icons-by-position-relative/meetup-icon.html
[10]: http://www.ibm.com/developerworks/cn/web/wa-mean5/signup-after.jpg
[11]: http://www.ibm.com/developerworks/cn/web/wa-mean5/index.html#tokens
[12]: http://en.wikipedia.org/wiki/Public_key_infrastructure
[13]: visit%20https://secure.meetup.com/meetup_api/oauth_consumers/create/
[14]: http://www.ibm.com/developerworks/cn/web/wa-mean5/oauth-meetup-register-keys.jpg
[15]: http://www.ibm.com/developerworks/cn/web/wa-mean5/index.html#N101F7
[16]: https://dev.twitter.com/web/sign-in/implementing
[17]: https://developers.facebook.com/docs/facebook-login/access-tokens
[18]: http://www.ibm.com/developerworks/cn/web/wa-mean2/index.html
[19]: http://www.ibm.com/developerworks/apps/download/index.jsp?contentid=1003628&filename=wa-mean5.zip&method=http&locale=zh_CN