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

图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 = "[email protected]";       //设置用户邮箱
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端点

时间: 2024-10-12 23:28:37

MEAN全栈开发 之 用户认证篇的相关文章

Python全栈开发记录_第一篇

Python全栈开发记录只为记录全栈开发学习过程中一些难和重要的知识点,还有问题及课后题目,以供自己和他人共同查看.(代码行数:70行) 知识点1:优先级:not>and 短路原则:and:如果第一个条件的结论为假,那么 and 前后两个条件组成的表达式计算结果一定为假,后面的条件计算机不会进行计算 or:如果第一个条件的结论为真,那么or 前后两个条件组成的表达式计算结果一定为真,后面的条件计算机不会进行计算 知识点2:python区分大小写,常量需全部字母大写(默认这样写) python换行

python全栈开发【第四篇】Python流程控制

十二 流程控制之if-else 既然我们编程的目的是为了控制计算机能够像人脑一样工作,那么人脑能做什么,就需要程序中有相应的机制去模拟.人脑无非是数学运算和逻辑运算,对于数学运算在上一节我们已经说过了.对于逻辑运算,即人根据外部条件的变化而做出不同的反映,比如 1 如果:女人的年龄>30岁,那么:叫阿姨 age_of_girl=31 if age_of_girl > 30: print('阿姨好') 2 如果:女人的年龄>30岁,那么:叫阿姨,否则:叫小姐 age_of_girl=18

python全栈开发【第十篇】Python常用模块二(时间、random、os、sys和序列化)

一.time模块 表示时间的三种方式: 时间戳:数字(计算机能认识的) 时间字符串:t='2012-12-12' 结构化时间:time.struct_time(tm_year=2017, tm_mon=8, tm_mday=8, tm_hour=8, tm_min=4, tm_sec=32, tm_wday=1, tm_yday=220, tm_isdst=0)像这样的就是结构化时间 #time模块的常用方法及三种时间之间的转换 import time # 对象:对象.方法 # --------

python全栈开发【第十一篇】Python常用模块三(hashlib,configparser,logging)

hashlib模块 hashlib提供了常见的摘要算法,如md5和sha1等等. 那么什么是摘要算法呢?摘要算法又称为哈希算法.散列算法.它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用16进制的字符串表示). 注意:摘要算法不是一个解密算法.(摘要算法,检测一个字符串是否发生了变化) 应涂:1.做文件校验 2.登录密码 密码不能解密,但可以撞库,用'加盐'的方法就可以解决撞库的问题.所有以后设置密码的时候要设置的复杂一点. #用户密码 import hashlib # md5

Python全栈开发记录_第九篇(类的基础_封装_继承_多态)

有点时间没更新博客了,今天就开始学习类了,今天主要是类的基础篇,我们知道面向对象的三大特性,那就是封装,继承和多态.内容参考该博客https://www.cnblogs.com/wupeiqi/p/4493506.html 之前我们写的都是函数,可以说是面向过程的编程,需要啥功能就直接写啥,但是我们在编写程序的过程中会发现如果多个函数有共同的参数或数据时,我们也必须多次重复去写,此时如果用面向对象的编程方式就会好很多,这也是面向对象的适用场景. 面向对象三大特性: 一.封装(顾名思义就是将内容封

python全栈开发【第三篇】Python运算符

计算机可以进行的运算有很多种,不只是加减乘除,它和我们人脑一样,也可以做很多运算. 种类:算术运算,比较运算,逻辑运算,赋值运算,成员运算,身份运算,位运算,今天我们先了解前四个. 算术运算: a=10,b=20 赋值运算: 比较运算: 逻辑运算:   原文地址:https://www.cnblogs.com/xiaohema/p/8452952.html

python全栈开发【第五篇】Python可变数据类型和不可变数据类型

1.可变数据类型:在id不变的情况下,value可改变(列表和字典是可变类型,但是字典中的key值必须是不可变类型) 2.不可变数据类型:value改变,id也跟着改变.(数字,字符串,布尔类型,都是不可类型) 原文地址:https://www.cnblogs.com/xiaohema/p/8452966.html

python全栈开发【第七篇】Python文件操作

一.文件处理流程 1.打开文件,得到文件句柄并赋值给一个变量 2.通过句柄对文件进行操作 3.关闭文件 r模式,默认模式,文件不存在则报错 w模式,文件不存在则创建,文件存在则覆盖 a模式,文件不存在则创建,文件存在则不会覆盖,写内容会以追加的方式写(写日志文件的时候常用),追加模式是一种特殊的写模式 b(rb,wb,ab)模式:不用加encoding:utf-8 f=open('c.txt','rb') # print(f.read()) print(f.read().decode()) f=

python全栈开发【第六篇】Python字符编码

1.内存和硬盘都是用来存储的. CPU:速度快 硬盘:永久保存 2.文本编辑器存取文件的原理(nodepad++,pycharm,word) 打开编辑器就可以启动一个进程,是在内存中的,所以在编辑器编写的内容也都是存放在内存中的,断电后数据就丢失了.因而需要保存在硬盘上,点击保存按钮或快捷键,就把内存中的数据保存到了硬盘上.在这一点上,我们编写的py文件(没有执行时),跟编写的其他文件没有什么区别,都只是编写一堆字符而已. 3.python解释器执行py文件的原理,例如python  test.