MEAN全栈开发 之 用户认证篇

作者:Soaring_Tiger
http://blog.csdn.net/Soaring_Tiger/article/details/51418209
本篇将覆盖以下内容(阅读本文需要有一定Express、Mongoose基础):

  1. 在 MEAN全栈开发中添加用户认证
  2. 在Express中使用Passport模块管理用户认证
  3. 在Exrpess中生成JSON Web Tokens(JWT)
  4. 实现用户注册与登录
  5. 在Angular当中使用 local storage管理用户session

1.1 在MEAN开发中如何实现用户认证?

对于Angular这样的单页面程序(SPA)而言,用户认证似乎是个麻烦事情,因为所有的(前端)程序都会被发送到浏览器上,所以怎样隐藏你想要隐藏的东西是个问题。

1.1.1我们先来看看传统的基于服务端的程序是如何实现的?

如果你比较熟悉传统的基于服务端的Web开发(比如世界上最好的语言PHP),那么你可能会对单页面应用(SPA)如何实现用户认证感到困惑。
传统的基于服务端的Web开发的用户认证流程一般是这样的:
(1)用户在表单上输入用户名与密码并提交到服务器;
(2)服务器上的程序会通过数据库校验用户名和密码以及权限是否正确;
(3)如果校验成功,服务器在用户的session上做标记并告诉用户他已经登录成功了;
(4)在用户浏览页面的时候,浏览器会将cookie发送到服务端,服务端会校验用户的session以及浏览权限,并将页面返回给用户。
那么,像MEAN全栈开发模式下又该怎么做呢?

1.1.2 MEAN全栈的用户认证实现方式

MEAN全栈的用户认证面临着两个问题:

  1. 通过Express实现的API是无状态的,也就是没有用户session的概念。
  2. 单页面程序(SPA)的编程逻辑已经传送到浏览器了,所以你无法限制这些已经在浏览器端的代码。

符合逻辑的解决方案是在浏览器端保持某种用户session状态,让前端程序决定什么可以显示给用户、什么不能显示给用户,这与服务端控制的方式有所区别,但是这是最主要的变化所在。
一个比较好并且安全的办法是采用JSON Web Token(JWT)来在客户端保存用户数据。对于JWT的细节我们在后面再谈,现在你只要知道它是一个加密了的JSON对象就行了。

管理登录流程

图5 JWT登录流程示意图图5
图5 示意了一个登录流程:(1)用户通过API把其身份验证信息提交给服务器;(2)服务器通过数据库校验用户身份信息;(3)服务器将一个令牌(token)返回给客户端;(4)客户端将令牌(token)保存并在下次需要时使用。
其实,整个流程与传统的服务器实现方案很相似,只是把用户session存在了客户端上。

依靠用户认证信息展示内容

图6:依靠JWT里的信息,SPA能够决定用户可以看到什么信息图6
如图6所示,在用户会话过程当中(user session),当用户要看新的页面的时候,前端程序根据JWT中保留的信息就能判断用户是否有权限进行浏览。
比起传统实现方法不同的是,除非用户需要通过API获取数据库里的信息,否则MEAN的服务端不用关心用户在看什么东西。

安全的调用API

如果应用程序的某些部分是对特定用户设限的,那么对于无状态的API而言,对API的每一次调用都需要知道调用权限,这个时候JWT就派上用场了。如图7所示,在调用需要认证的API端点时,客户端会发送JWT,而服务端通过解码JWT来验证用户的请求。
图7:在调用需要认证的API端点时,客户端会发送JWT,而服务端通过解码JWT来验证用户的请求 图7

ok,上面的部分介绍完了基本概念,我们已经大致上知道要干些什么了。下面我们就要一步步的实现这些过程。

2. 建立用于MongoDB的用户数据模型 (User Schema)

用户名和密码通常存储在数据库当中,在MEAN全栈开发里我们需要通过Mongoose来建一个模。特别需要提醒的是:密码在数据库中一定不要用明文保存!,因为这会带来巨大的安全漏洞。

2.1 单向密码加密:哈希+盐

要提高密码的安全度有一个办法:对密码进行单向加密。单向加密可以防止任何人解密,同时又非常容易验证密码。当用户进行登录的时候,程序可以对密码进行加密,并且比对已经存好的值。
当然,如果只是简单的加密还是不够的,因为如果有很多人用了同样的密码(比如:123456),那么加密出来的字符串就会一模一样,而黑客也就能轻易的对弱密码找出加密的模式。
这个时候就需要靠“盐(Salt)”来帮忙了,所谓“盐”就是一个在用户密码被加密之前针对每一个用户随机生成的字符串,而与密码混合生成的结果就是“哈希(Hash)”,如图8所示。
图8:哈希+盐 过程图8
“盐”和“哈希”被一同存储在数据库当中,而不是仅仅一个“password”字段,通过以上过程,所有的“哈希”都是独一无二的,这样就很好的保护了密码。

2.2 创建Mongoose模型

我们创建的userSchema 包含
用户名(name)、电子邮件(email)、哈希(hash)、盐(salt)等几个字段。其中email为必要而且是唯一的字段,name为必要字段。

var mongoose = require( 'mongoose' );
var userSchema = new mongoose.Schema({
  email: {
    type: String,
    unique: true,
    required: true
}, name: {
    type: String,
    required: true
  },
  hash: String,
  salt: String
});

2.3 用Mongoose 方法设置加密路径

Mongoose允许用户给schema添加自定义方法,例如下面代码当中的“setPassword”方法

var User = mongoose.model('User');     //实例化用户模型
var user = new User();                 //创建新用户
user.name = "User’s name";             //设置用户名  
user.email = "test@example.com";       //设置用户邮箱
user.setPassword("myPassword");        //调用自定义setPassword方法设置用户密码
user.save();                           //保存用户                        
下面我们再来看看具体如何给Mongoose Schema添加方法

在Schema被定义之后,在数据模型(model)被编译之前,我们可以给Schema添加方法。下面的代码示例了如何给userSchema 添加 setPassword方法:

userSchema.methods.setPassword = function(password){
  this.salt = SALT_VALUE;
  this.hash = HASH_VALUE;
};

对于Javascript而言,”this”在Mongoose中实际指的是模型本身,在本例当中即userSchema。
在我们保存用户资料之前,我们还必须生成一个随机的“盐”,以及把密码加密之后的“哈希”。幸运的是,Node.JS有个原生库就专干此事:crypto。

使用crypto进行加密

crypto顾名思义就是加密,它提供了一系列方法用于处理数据加密;让我们先来看看下面这两个:

  • randomBytes —— 生成一个足够“健壮”的字符串作为“盐”
  • pbkdf2Sync —— 通过 密码(password)和 盐(salt)构建一个“哈希”;pbkdf2 即 password-based key derivation function 2 的缩写,这是一个加密相关的工业标准。

首先,我们需要在文件开头先引入 crypto库

var mongoose = require( 'mongoose' );
var crypto = require('crypto');

然后,我们的setPassword函数要更新一下,生成的“盐”是一个16位的字符串,接着在用“盐”把密码“哈希”加密:

userSchema.methods.setPassword =function(password){ this.salt = crypto.randomBytes(16).toString('hex'); this.hash = crypto.pbkdf2Sync(password, this.salt, 1000,64).toString('hex');
};

现在,用户输入的密码就被安全的加密了,原始密码不被保存在任何地方(包括内存),也就是说没人能获取这个原始密码了。

2.4 验证提交的密码

在加密保存了用户密码之后,要做的另一件事就是在用户下次登录的时候验证用户密码,我们可以写一个简单的Mongoose 方法来做这件事:

userSchema.methods.validPassword = function(password) {
var hash = crypto.pbkdf2Sync(password, this.salt, 1000, 64).toString('hex'); return this.hash === hash;
};

上面的代码就是把用户输入的密码加上盐之后做成“哈希”,再与原始“哈希”进行比对就醒了。怎样?代码实现起来很简单吧^_^

那么接下来我们还要搞定最后一件事情,那就是生成 JSON Web Token(JWT) 。

2.5 生成JSON Web Token

所谓JWT(发音念“jot”)的作用是在服务端和我们的客户端SPA程序之间传递数据。JWT当然也能用于在服务端和客户端之间进行用户验证。
让我们来看看JWT的构成:

JWT的三个组成部分

先看一个实际的JWT例子:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NTZiZWRmNDhmOTUzOTViMTlhNjc1 ODgiLCJlbWFpbCI6InNpbW9uQGZ1bGxzdGFja3RyYWluaW5nLmNvbSIsIm5hbWUiOiJTaW1vbiBIb 2xtZXMiLCJleHAiOjE0MzUwNDA0MTgsImlhdCI6MTQzNDQzNTYxOH0.GD7UrfnLk295rwvIrCikbk AKctFFoRCHotLYZwZpdlE

看晕了吧?其实你要是眼力好呢就会发现这个超级长的字符串实际上是被两个“.”给分割开来的三部分组成的,这三部分分别是:

  • 头(Header)—— 一个经过编码并包含了类型和哈希算法的JSON对象;
  • 有效数据(Payload)—— 一个被编码过的包含令牌(token)信息的JSON对象;
  • 签名(Signature)——
    一个用服务器上的密钥把Header和Payload加密后的“哈希”。

注意:前两个部分并没有被加密——而是仅仅被编码了,这意味着他们是很容易被解码的——绝大部分“现代浏览器”都有一个内置的函数叫atob()可以解码Base64字符串。
第三部分的签名是被加密过的,要想解密就必须使用服务器上的密钥,而所谓密钥嘛那就是只能在服务器上用并且不能公之于众的家伙。

听起来有点点复杂啊!不用担心,有个好消息是你只用安装一个库就能轻松搞定JWT了。

在服务端生成JWT

安装JWT生成库的命令如下:

$ npm install jsonwebtoken --save

然后在代码中引入jsonwebtoken库

var mongoose = require( 'mongoose' ); var crypto = require('crypto');
var jwt = require('jsonwebtoken');

最后我们要给User模型添加一个generateJwt方法。要想顺利生成JWT我们需要提供有效数据(Payload)以及密钥。在有效数据当中我们发送用户的 _id、email、name。我们还应该设置一个令牌的过期时间——当令牌过期之后,用户需要重新登录并获取新的令牌,我们用JWT有效数据(Payload)的保留关键字“exp”来包存这个过期时间。具体实现请看代码:

userSchema.methods.generateJwt = function() {
  var expiry = new Date();
      expiry.setDate(expiry.getDate() + 7); //将过期时间设为7天

return jwt.sign({
  _id: this._id,
  email: this.email,
  name: this.name,
  exp: parseInt(expiry.getTime() / 1000),
}, 'thisIsSecret' ); //"thisIsSecret"是密钥
};

当然,上面的代码有点点问题:密钥是以明文方式出现在代码里面的,我们接下来就解决它。

将密钥以环境变量的方式保存

如果你想对代码进行版本控制——比如通过GitHub,那你千万不要把密钥写死在你的代码里面,-_-# 要想保护你的密钥,比较稳妥的办法是通过环境变量来进行设置。设置环境变量比较简单,在项目的根目录下面见一个.env文件,再把密钥写在里面:

JWT_SECRET=thisIsSecret

然后,要保证这个.env文件不会被上传到Github上,你还得写个.gitignore文件:

# Dependency directory
node_modules
# Environment variables
.env

要想读取.env文件,你还要安装一个库:dotenv

$ npm install dotenv --save

再通过dotenv把环境变量读进来:

require('dotenv').load();
var express = require('express');

最后看看我们引入环境变量后得代码:

userSchema.methods.generateJwt = function() {
  var expiry = new Date();
  expiry.setDate(expiry.getDate() + 7);
  return jwt.sign({
    _id: this._id,
    email: this.email,
    name: this.name,
    exp: parseInt(expiry.getTime() / 1000),
}, process.env.JWT_SECRET); };

当然,设置环境变量的方法还有很多,本文暂不赘述。下面我们要谈谈如何使用Passport库来管理用户认证。

3.通过Passport建立用户认证API

Passport 是由Jared Hanson
设计开发的Node.JS用户认证库,其优点是可以使用多种认证策略,包括:

  • Facebook
  • Twitter
  • Oauth
  • 本地用户名及密码

本文暂时只介绍本地用户名密码的认证策略。

3.1安装并配置Passport

安装命令如下:

$ npm install passport --save
$ npm install passport-local --save

装完之后,我们就可以配置passport了。

创建Passport配置文件

我们在项目文件夹里建一个config目录,在该目录中建一个passport.js文件,在该文件的最上面,我们引入要用到的库:

var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var mongoose = require('mongoose');
var User = mongoose.model('User');
配置本地策略

配置代码的基础框架如下:

passport.use(new LocalStrategy({},
  function(username, password, done) {
  }
));

缺省情况下,Passport的本地策略中使用“username”和“password”作为字段名,在本例当中,我们用电子email取代username作为登录名,所以要做些改动,好在Passport允许我们重载username,代码如下:

passport.use(new LocalStrategy(
    usernameField: 'email'
  },
  function(username, password, done) {
  }
));

接下来的主函数,主要依靠Mongoose去查找对应的用户名、密码,我们要完成以下几件事:

  • 通过用户提供的email查找用户档案;
  • 验证密码是否正确;
  • 如果验证无误,返回用户对象;
  • 如果有误,则报错。

由于email是唯一的,所以我们可以用Mongoose的findOne函数来查找用户,然后我们可以用上一节里写的validPassword函数来验证用户提供的密码是否正确,代码如下:

passport.use(new LocalStrategy({
    usernameField: 'email'
  },
  function(username, password, done) {
    User.findOne({ email: username }, function (err, user) {
      if (err) { return done(err); }

if (!user) {
  return done(null, false, {
    message: '错误的用户名或密码.'
  });
}

if(!user.validPassword(password)) {
  return done(null, false, {
  message: '错误的用户名或密码.'
});
}
      return done(null, user);
    });
} ));

当然在主文件app.js中还得加上几句代码:

var passport = require('passport');
require('./app_api/config/passport');
app.use(passport.initialize());

这样Passport就算安装、配置、初始化成功了!下面我们要搞的是用户登录的API端点。

3.2 建立返回JSON Web Tokens的API端点

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值