美文网首页
“真实世界”全栈开发-3.3-用户模型

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

作者: 桥头堡2015 | 来源:发表于2018-02-04 15:13 被阅读26次

    数据库本应用选用的是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的缩写。其令牌由后端签署然后返回给前端,里面包括三项数据:

    • 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')来获取对应的用户模型了。

    相关文章

      网友评论

          本文标题:“真实世界”全栈开发-3.3-用户模型

          本文链接:https://www.haomeiwen.com/subject/hzhmzxtx.html