数据库本应用选用的是MongoDB。MongoDB是NoSQL的,所以我们不用写SELECT
、JOIN
之类的SQL语句;数据库的集合(collection,相当于关系数据库中的table)中的文档(document,相当于table中的一行)也不必遵循统一的模式(schema)。话虽如此,我们会使用mongoose.js来管理和MongoDB的交互。有了Mongoose,我们可以为数据定义模式及检验方法,确保数据的一致性。
前一部分提到的应用的每一个功能,我们都按照下面的三个步骤来开发:
- 在Model层为对应的数据创建Mongoose模式
- 在Control层为数据开发处理逻辑
- 创建对应的路由,将端点暴露给使用者(前端)
本文剩下的部分我们完成与用户相关的功能。
注意:下文以及后续博文里,提到数据时,我们会反复用到两个类似的词:“模式”和“模型”。分别对应的英文单词为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}
会添加两个字段createdAt
和updatedAt
,当模型有改动时,两者会自动更新。最后一行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);
上面,我们为username
和email
添加了检验要求。代码很容易懂,唯一值得说明的是index: true
这个选项,它使得使用这两个字段的查询更加优化。
使用Mongoose插件
眼尖的读者可能发现了,上面的代码无法保证用户名和邮箱的唯一性,这是因为Mongoose没有这样的内置验证选项。所以我们必须依赖插件,例如mongoose-unique-validator
。如果你查看过package.json
,就会知道其实这个包在前面运行npm install
时就已经作为项目种子的依赖包安装过了。在代码里使用它,只需要两步:
require
- 往对应的模式上注册这一插件
再次更改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的缩写。其令牌由后端签署然后返回给前端,里面包括三项数据:
-
id
- 数据库中对应的用户文档的ID -
username
- 用户名 -
exp
- 一个UNIX时间戳(秒级),记录该令牌的失效时刻。这里我们将其设定为60天之后。
令牌中的这些数据,前端后端都可以读取。后端可以验证前端发送过来的令牌是不是自己签署的,从而确信令牌中的数据是不是真实的。
使用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')
来获取对应的用户模型了。
网友评论