Node Hero 系列之:使用 Passport.js 做 Node.js 身份验证

本文是 Node Hero 系列教程的第八篇——通过这些章节,你将学会如何开始使用 Node.js 发布软件产品。

本篇 Passport.js 教程将带你一步步使用 Redis 搭建 Node.js 本地身份验证策略。你将学会如何使用 Passport.js 创建身份验证界面,用户在界面里提供用户名和密码。虽然看起来复杂,但是在 Node.js 中实现身份验证机制还是比较容易的。

本系列全部文章:

  1. Node Hero 系列之:开始使用 Node.js
  2. Node Hero 系列之:npm 教程
  3. Node Hero 系列之:Node.js 异步编程
  4. Node Hero 系列之:你的第一个 Node.js HTTP 服务器
  5. Node Hero 系列之:数据库教程
  6. Node Hero 系列之:Node.js request 模块教程
  7. Node Hero 系列之:Node.js 项目文件组织教程
  8. Node Hero 系列之:使用 Passport.js 做 Node.js 身份验证
  9. Node Hero 系列之:Node.js 单元测试教程
  10. Node Hero 系列之:Node.js 调试教程
  11. Node Hero 系列之:Node.js 安全教程
  12. Node Hero 系列之:如何使用 Heroku 或 Docker 部署 Node.js
  13. Node Hero 系列之:Node.js 应用监控

使用的技术

在一头扎进 Passport.js 身份验证教程之前,先让我们看看这章会用到的技术。

Passport.js 是什么?

  • Passport.js 是一个简单的、非侵入式的 Node.js 身份验证中间件。
  • Passport.js 可以集成到任何基于 Express.js 的 web 应用中。
Passport.js is an authentication middleware for Node.js

Passport 是一个 Node.js 身份验证中间件,我们用它来做会话管理。

Redis 是什么?

  • Redis 是一个开源的(BSD 开源协议)内存数据结构存储器,可用作数据库、缓存和消息代理。
  • Redis 支持不同类型的抽象数据结构,例如字符串、哈希、列表、集合、带范围查询的有序集合、位图、hyperlog 以及带半径查询的地理空间索引。

我们会把用户会话信息保存在 Redis 而不是进程内存里。这样我们的应用更容易扩展。

需要身份验证的演示程序

为了演示,我们做一个只包含如下功能的应用:

  • 有个登录表单
  • 有两个受保护的页面:exposes two protected pages:
    • 个人信息页
    • 受保护的便签

项目结构

在前一篇的 Node Hero 系列教程中,你已经学过如何组织 Node.js 项目了,那就让我们学以致用吧!
我们将采用如下结构:

├── app
|   ├── authentication
|   ├── note
|   ├── user
|   ├── index.js
|   └── layout.hbs
├── config
|   └── index.js
├── index.js
└── package.json

如你所见,我们是根据功能来组织文件和目录的。我们将有一个用户页面,一个便签页面和一些身份验证相关的功能。
(下载完整源码: https://github.com/RisingStack/nodehero-authentication)

Node.js 身份验证流程

我们的目标是使用 Passport.js 在应用中实现如下的身份验证流程:

  1. 用户输入用户名和密码
  2. 应用检查用户名和密码是否匹配
  3. 如果匹配,应用会发送一个Set-Cookie头,用来验证后续页面
  4. 当用户访问相同域名的页面时,之前设置的 cookie 会加入到所有请求中
  5. 用该 cookie 验证受限的页面

使用 Passport.js 在 Node.js 应用中建立这样的身份验证策略,按照以下三个步骤:

步骤 1: 安装 Express

我们将使用 Express 作为服务框架——你可以在我们的Express 教程学习更多相关主题。

// 文件:app/index.js
const express = require('express')
const passport = require('passport')
const session = require('express-session')
const RedisStore = require('connect-redis')(session)

const app = express()
app.use(session({
  store: new RedisStore({
    url: config.redisStore.url
  }),
  secret: config.redisStore.secret,
  resave: false,
  saveUninitialized: false
}))
app.use(passport.initialize())
app.use(passport.session())

我们在这里做了什么?

首先,我们引入了会话管理所需的所有依赖。之后我们创建了一个express-session实例,用来保存会话。

我们使用 Redis 做背后的存储,但是你也可以用其他工具,比如 MySQL 或者 MongoDB。

步骤 2:安装 Passport.js

Passport.js 是一个使用了插件的库的很好的例子。在这篇教程里,我们添加了 passport-local 模块,很容易通过用户名和密码集成简单的本地身份验证策略 。

为简单起见,在这个例子中我们没有使用另外的存储,只用到用户实例的内存。在实际应用中,findUser 将会在数据库里查找用户。

// 文件:app/authenticate/init.js
const passport = require('passport')
const bcrypt = require('bcrypt')
const LocalStrategy = require('passport-local').Strategy

const user = {
  username: 'test-user',
  passwordHash: 'bcrypt-hashed-password',
  id: 1
}

passport.use(new LocalStrategy(
 (username, password, done) => {
    findUser(username, (err, user) => {
      if (err) {
        return done(err)
      }

      // 用户未找到
      if (!user) {
        return done(null, false)
      }

      // 一定要使用哈希后的密码和固定时间的比较算法
      bcrypt.compare(password, user.passwordHash, (err, isValid) => {
        if (err) {
          return done(err)
        }
        if (!isValid) {
          return done(null, false)
        }
        return done(null, user)
      })
    })
  }
))

一旦 findUser 返回了用户对象,剩下的只需要比较用户哈希后的密码和真实密码,看是否匹配。一定要存储哈希后的密码,并且使用固定时间的比较算法以防时序攻击

如果匹配,我们就让用户通过(返回用户对象给 Passport——return done(null, user)),否则返回一个未授权错误(返回 null 给 Passport——return done(null))。

步骤 3:添加受保护端点

为了添加受保护端点,我们采用了 Express 用到的中间件模式。为此,我们先创建身份验证中间件:

// 文件:app/authentication/middleware.js
function authenticationMiddleware () {
  return function (req, res, next) {
    if (req.isAuthenticated()) {
      return next()
    }
    res.redirect('/')
  }
}

当用户通过验证时(有正确的 cookie),它只做一件事:调用下一个中间件。否则就重定向到登录页面。

使用它也很简单,只要在路由定义的地方加上中间件实例就行了。

// 文件:app/user/init.js
const passport = require('passport')

app.get('/profile', passport.authenticationMiddleware(), renderProfile)

总结:用 Passport.js 验证身份

在这篇 Passport.js 教程中,你学会了如何在 Node.js 应用中添加基本的身份验证。后续你可以扩展到其他身份验证策略,比如 Facebook 或 Twitter。你可以在 http://passportjs.org/ 找到更多策略。

完整可运行的例子的 Github 地址是 https://github.com/RisingStack/nodehero-authentication

预告

Node Hero 的下一篇是关于 Node.js 单元测试的,你将学会单元测试、测试金字塔、测试替身等概念。

欢迎在评论区分享你的问题和反馈。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容