MEAN全栈开发 之 用户认证篇
Posted 宇宙歌者
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MEAN全栈开发 之 用户认证篇相关的知识,希望对你有一定的参考价值。
作者:Soaring_Tiger
http://blog.csdn.net/Soaring_Tiger/article/details/51418209
本篇将覆盖以下内容(阅读本文需要有一定Express、Mongoose基础):
- 在 MEAN全栈开发中添加用户认证
- 在Express中使用Passport模块管理用户认证
- 在Exrpess中生成JSON Web Tokens(JWT)
- 实现用户注册与登录
- 在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全栈的用户认证面临着两个问题:
- 通过Express实现的API是无状态的,也就是没有用户session的概念。
- 单页面程序(SPA)的编程逻辑已经传送到浏览器了,所以你无法限制这些已经在浏览器端的代码。
符合逻辑的解决方案是在浏览器端保持某种用户session状态,让前端程序决定什么可以显示给用户、什么不能显示给用户,这与服务端控制的方式有所区别,但是这是最主要的变化所在。
一个比较好并且安全的办法是采用JSON Web Token(JWT)来在客户端保存用户数据。对于JWT的细节我们在后面再谈,现在你只要知道它是一个加密了的JSON对象就行了。
管理登录流程
图5
图5 示意了一个登录流程:(1)用户通过API把其身份验证信息提交给服务器;(2)服务器通过数据库校验用户身份信息;(3)服务器将一个令牌(token)返回给客户端;(4)客户端将令牌(token)保存并在下次需要时使用。
其实,整个流程与传统的服务器实现方案很相似,只是把用户session存在了客户端上。
依靠用户认证信息展示内容
图6
如图6所示,在用户会话过程当中(user session),当用户要看新的页面的时候,前端程序根据JWT中保留的信息就能判断用户是否有权限进行浏览。
比起传统实现方法不同的是,除非用户需要通过API获取数据库里的信息,否则MEAN的服务端不用关心用户在看什么东西。
安全的调用API
如果应用程序的某些部分是对特定用户设限的,那么对于无状态的API而言,对API的每一次调用都需要知道调用权限,这个时候JWT就派上用场了。如图7所示,在调用需要认证的API端点时,客户端会发送JWT,而服务端通过解码JWT来验证用户的请求。
图7
ok,上面的部分介绍完了基本概念,我们已经大致上知道要干些什么了。下面我们就要一步步的实现这些过程。
2. 建立用于MongoDB的用户数据模型 (User Schema)
用户名和密码通常存储在数据库当中,在MEAN全栈开发里我们需要通过Mongoose来建一个模。特别需要提醒的是:密码在数据库中一定不要用明文保存!,因为这会带来巨大的安全漏洞。
2.1 单向密码加密:哈希+盐
要提高密码的安全度有一个办法:对密码进行单向加密。单向加密可以防止任何人解密,同时又非常容易验证密码。当用户进行登录的时候,程序可以对密码进行加密,并且比对已经存好的值。
当然,如果只是简单的加密还是不够的,因为如果有很多人用了同样的密码(比如:123456),那么加密出来的字符串就会一模一样,而黑客也就能轻易的对弱密码找出加密的模式。
这个时候就需要靠“盐(Salt)”来帮忙了,所谓“盐”就是一个在用户密码被加密之前针对每一个用户随机生成的字符串,而与密码混合生成的结果就是“哈希(Hash)”,如图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用户认证库,其优点是可以使用多种认证策略,包括:
- 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端点
以上是关于MEAN全栈开发 之 用户认证篇的主要内容,如果未能解决你的问题,请参考以下文章