“真实世界”全栈开发-3.3-用户模型

2018-02-04  本文已影响26人  桥头堡2015

数据库本应用选用的是MongoDB。MongoDB是NoSQL的,所以我们不用写SELECTJOIN之类的SQL语句;数据库的集合(collection,相当于关系数据库中的table)中的文档(document,相当于table中的一行)也不必遵循统一的模式(schema)。话虽如此,我们会使用mongoose.js来管理和MongoDB的交互。有了Mongoose,我们可以为数据定义模式及检验方法,确保数据的一致性。

前一部分提到的应用的每一个功能,我们都按照下面的三个步骤来开发:

  1. 在Model层为对应的数据创建Mongoose模式
  2. 在Control层为数据开发处理逻辑
  3. 创建对应的路由,将端点暴露给使用者(前端)

本文剩下的部分我们完成与用户相关的功能。

注意:下文以及后续博文里,提到数据时,我们会反复用到两个类似的词:“模式”和“模型”。分别对应的英文单词为schema和model。正如上面所说以及下面将会看到的,使用Mongoose,我们需要先定义“模式”,然后把它注册成“模型”以供与数据库交互。如果现在觉得糊涂,请不要灰心,多看看代码就会明白了。

为用户数据创建模式

我们首先为用户模型定义Mongoose模式,然后把它登记到Mongoose对象上,之后,就可以在整个后端以面向对象的方式来访问MongoDB中的用户数据了。

新建文件models/User.js,内容如下:

const mongoose = require('mongoose');

// 定义模式
const UserSchema = new mongoose.Schema({
  username: String,
  email: String,
  bio: String,
  image: String,
  hash: String,
  salt: String
}, {timestamps: true});

// 将模式注册成模型
mongoose.model('User', UserSchema);

第二个参数{timestamps: true}会添加两个字段createdAtupdatedAt,当模型有改动时,两者会自动更新。最后一行mongoose.model('User', UserSchema);将模式注册到mongoose对象上。这样,在应用的任何位置,我们都可以通过mongoose.model('User')获取用户模型。

为用户模式创建检验选项

接下来我们为用户模式创建“检验选项”,避免往数据库写入不合法的数据。关于数据检验和Mongoose内置检验选项的更多信息,请见这里

models/User.js做如下更改:

const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
  // ***
  username: {type: String, lowercase: true, required: [true, "can't be blank"], match: [/^[a-zA-Z0-9]+$/, 'is invalid'], index: true},
  email: {type: String, lowercase: true, required: [true, "can't be blank"], match: [/\S+@\S+\.\S+/, 'is invalid'], index: true},
  // ***
  bio: String,
  image: String,
  hash: String,
  salt: String
}, {timestamps: true});

mongoose.model('User', UserSchema);

上面,我们为usernameemail添加了检验要求。代码很容易懂,唯一值得说明的是index: true这个选项,它使得使用这两个字段的查询更加优化。

使用Mongoose插件

眼尖的读者可能发现了,上面的代码无法保证用户名和邮箱的唯一性,这是因为Mongoose没有这样的内置验证选项。所以我们必须依赖插件,例如mongoose-unique-validator。如果你查看过package.json,就会知道其实这个包在前面运行npm install时就已经作为项目种子的依赖包安装过了。在代码里使用它,只需要两步:

  1. require
  2. 往对应的模式上注册这一插件

再次更改models/User.js文件如下:

const mongoose = require('mongoose');
// +++
const uniqueValidator = require('mongoose-unique-validator');
// +++

const UserSchema = new mongoose.Schema({
  // ***
  username: {type: String, lowercase: true, unique: true, required: [true, "can't be blank"], match: [/^[a-zA-Z0-9]+$/, 'is invalid'], index: true},
  email: {type: String, lowercase: true, unique: true, required: [true, "can't be blank"], match: [/\S+@\S+\.\S+/, 'is invalid'], index: true},
  // ***
  bio: String,
  image: String,
  hash: String,
  salt: String
}, {timestamps: true});

// +++
UserSchema.plugin(uniqueValidator, {message: 'is already taken.'});
// +++

mongoose.model('User', UserSchema);

为用户模式创建辅助方法

接下来我们要为用户模式创建几个辅助方法。这些方法都与用户的密码设定和身份验证有关。

生成密码哈希值的方法

在数据库中,我们不能存储用户的密码明文,而是存储通过密码计算出的哈希值。这就要借助于Node内置的一个库crypto

models/User.js里,先导入这个库:

const mongoose = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');
// +++
const crypto = require('crypto');
// +++

我们接下来创建计算密码哈希值的方法。这个方法首先为每个用户随机生成盐值,然后用crypto.crypto.pbkdf2Sync方法计算哈希值。该方法接受5个参数:原始密码,盐值,迭代次数(这里用10k次),哈希值的长度(512),以及具体的哈希算法(这里用的是sha512)。

models/User.js里的具体实现如下:

// +++
UserSchema.methods.setPassword = function (password) {
  this.salt = crypto.randomBytes(16).toString('hex');
  this.hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
};
// +++

mongoose.model('User', UserSchema);

需要指出的是,setPassword并不是直接注册为UserSchema的一个实例方法(UserSchema.prototype.setPassword = ...),这是因为以后我们实例化的并不是用户模式,而是用户模型。注册模式时,Mongoose会自动把该模式methods中的方法都定义成对应模型的实例方法。另外,setPassword中的this指向的也是当前用户模型的对象。最后,由于ES6中的箭头函数没有自己的this,所以不能箭头函数不能用在这里。(实际上,箭头函数不能用于定义constructor,也应避免用于定义method)。

验证用户密码的方法

接下来我们要创建验证用户密码的辅助方法。

其逻辑非常简单,看看待验证的密码通过相同的哈希过程生成的值,是否等同于数据库中所存储的哈希值。

依然是在models/User.js文件里,加入下面的代码:

// +++
UserSchema.methods.validPassword = function (password) {
  const hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
  return this.hash === hash;
};
// +++

mongoose.model('User', UserSchema);

生成JWT令牌的方法

JWT是JSON Web Token的缩写。其令牌由后端签署然后返回给前端,里面包括三项数据:

令牌中的这些数据,前端后端都可以读取。后端可以验证前端发送过来的令牌是不是自己签署的,从而确信令牌中的数据是不是真实的。

使用jsonwebtoken包(同样已经安装)来签署(及验证)JWT令牌,后端需要一句口令:一个只有后端应用知道的随机字符串。根据config/index.js文件的设置,开发环境设置下的口令是"secret",生产环境里则需要读取对应的环境变量。

先把所需要的包和口令导入models/User.js:

const crypto = require('crypto');
// +++
const jwt = require('jsonwebtoken');
const secret = require('../config').secret;
// +++

然后创建生成JWT的方法。

// +++
UserSchema.methods.generateJWT = function() {
  const exp = new Date();
  exp.setDate(exp.getDate() + 60);
  return jwt.sign({
    id: this._id,
    username: this.username,
    exp: Math.floor(exp.getTime() / 1000)
  }, secret);
};
// +++

mongoose.model('User', UserSchema);

返回用户JSON对象的方法

最后一个辅助方法用来返回用户JSON对象,这一数据结构在用户成功登陆后会返回给前端,其中包括一枚令牌,用于以后的需要验证的操作,所以不要把它发送给错误的用户。

// +++
UserSchema.methods.toAuthJSON = function(){
  return {
    username: this.username,
    email: this.email,
    token: this.generateJWT(),
    bio: this.bio,
    image: this.image
  };
};
// +++

mongoose.model('User', UserSchema);

注册用户模型

我们的用户模型就已经创建好了。为了在后端应用中使用这一模型,需要运行models/User.js这一文件(完成所有的定义与注册)。

app.js中加入(确保模型的导入在路由的设定之前):

// +++
require('./models/User');
// +++

app.use(require('./routes'));

上面新加的require语句确保用户模式的定义和注册(即mongoose.modle('User', UserSchema))的运行。这样在之后的代码里,我们就可以通过mongoose.model('User')来获取对应的用户模型了。

上一篇下一篇

猜你喜欢

热点阅读