Node with React: Fullstack Web Development 课程手记(三)——MongoDB

上篇地址

MongoDB简介

  • MongoDB是一个基于文档(document)的数据库。在MongoDB中,数据是以Collection的形式来组织的,也就是一个Collection代表一种数据。一个Collection中的每条记录(document/record)不必拥有相同的字段,也就是说我们可以动态地为数据添加、减少或者修改字段。如下图所示,不同的User记录具备可以拥有不同的字段。
  • 我们使用mongoose来进行数据库的操作。这其中包括两个部分:js和数据库。js部分每个Model Class对应数据库部分的每个Collection,js部分的每个实例对应数据库部分的每条记录(record)。

add mongoDB

  • 使用mongoDB有两种方式:本地安装;远程安装。本次课采用后者,使用MongoDB后的系统架构如下图所示。
  • 登陆 mlab.com,创建账号,登陆,创建一个免费的database,进入其控制面板。创建管理员用户名和密码。done!
  • 在server端引入mongoose,并连接我们刚才创建的数据库。首先安装mongoose,npm install --save mongoose。 刚才在创建数据库成功的页面有这样一句话To connect using a driver via the standard MongoDB URI (what's this?):。这句话后面的内容就是我们要访问这个数据库的URI。把里面的<dbuser>dbpassword改为我们刚才创建管理员的用户名和密码,就可以访问了。因为这个信息也属于敏感信息,所以把这部分内容写在./config/keys.js中,在index.js中引入,并使用的代码如下所示:
const keys = require('./config/keys');
const mongoose = require("mongoose");
mongoose.connect(keys.mongoURI);复制代码
  • 这里看一下我们所处的状态和接下来要做的事情。首先我们有了用于存储数据的MongoDB和用于操作数据的mongoose。接下来我们要对访问的用户进行检查,检查他们是否在我们的存储记录中,如果在就让他登陆,如果不在点击授权,我们用授权返回的GoogleID为内容创建一条新的记录,那么当用户下次进入网站的时候就不必再次授权了。
  • 接下来创建model。MongoDB自身的collection是可以包含不同结构 的记录的,但是mongoose却需要预先定义collection的记录解构是什么样子的。因此这里需要预先设置Schema。传入的参数是一个对象,定义collection的各个key,及对应的数据类型。(允许在中途修改Schema)。这里创建一个新的文件./models/Users.js
const mongoose = require('mongoose');
const { Schema } = mongoose;
// es6 解构赋值 <=> const Schema = mongoose.Schema
const userSchema = new Schema({
    googleId: String
});复制代码
  • mongoose是通过创建一个class的方式创建一个collection的。接下来的代码创建一个名字为users的collection,使用的Schema就是上面创建的userSchema,这个Schema定义了这个collection的每个记录都包含一个类型为string,名为googleId的数据。
mongoose.model('users', userSchema);复制代码
  • 最后再index.js中引入./models/Users.js文件,以使这一堆代码运行。
require('./models/Users');复制代码
  • 然后我们要做的是就是要把从Google服务器拿到的id,存储为一个Collection为users的记录。我们是在./services/passport使用new GoogleStrategy()方法中的回调函数拿到用户资料的。因此你我们将会在那个回调函数中使用mongoose将数据存储到Collection为users数据库中。首先我们要拿到名为users的collection。代码如下,注意我们使用了同样的函数mongoose.use,这个函数当传入Schema时,是创建collection,当只传名字的时候,就是取到Collection。
const mongoose = require('mongoose');
const User = mongoose.model('users');复制代码
  • new GoogleStrategy()传入的回调函数中,我们创建一个user实例。注意,这里new User()是创建了一个JavaScript对象,并未将数据存入数据库中(参考上面mongoose vs mongoDB的图),要将数据写入数据库,必须调用这个对象的save方法。
new User({ googleId: profile.id }).save();复制代码
  • 注意我们是在./models/Users.js中定义名为users的collection的,但在./services/passport.js中使用了这个collection,因此在index.js中引入这两个文件时要注意先后顺序,前者要先引用。
  • 现在访问localhost:5000/auth/google,然后去mlab的面板上刷新,可以看到Collection目录下多了一条名为user的条目,点击进去可以看到有一条记录,其中的googleId就是你刚才用于授权的googleId账户的id。但是现在有一个问题,当我们重复这个操作,就会发现我们的数据库中多了一条重复的记录。而我们想要的结果是,如果已经有了相同的记录就不再创建记录。
  • 我们接着使用mongoose class的查询功能,检查当前用户是否存在,如果不存在才新建一个。逻辑变为:
User.findOne({ googleId: profile.id }).then((existingUser) => {
    if (!existingUser) {
        new User({
            googleId: profile.id
        }).save();
    }
});复制代码
  • 注意,所有的数据库操作都是异步的,mongoose为我们封装了Promise来对返回结果进行操作,因此这里将判断逻辑写在了then的回调函数中。
  • 还没完,我们还没有用户的信息传递给passport。如何把用户信息传递给passport呢。注意之前的回调函数中传入了done参数,done是一个函数,其第一个参数是为了传递错误信息,第二个参数是为了传输passport验证所需的信息。所以我们可以把user信息传入done的第二个参数,从而传递给passport,具体代码如下:
User.findOne({ googleId: profile.id }).then(existingUser => {
    if (!existingUser) {
        new User({
            googleId: profile.id
        })
            .save()
            .then(user => {
                done(null, user);
            });
    } else {
        done(null, existingUser);
    }
});复制代码
  • 为什么我们要搞数据库呢?——当然是为了验证流程了。我们这次采用的是使用cookie的验证流程,而所有数据库这一套东西都是为了产生cookie。
  • 用户访问网站,通过查找数据库来判断是新用户还是老用户。
  • 是新用户,那么在数据库产生一个新的记录,并用这个新的数据库来产生cookie,并返回给浏览器。以后浏览器在对这个服务器产生其他请求时,cookie将自动携带,服务器就能识别这个请求是属于这个用户了。
  • 如果是老用户,直接从数据库中取出用户信息,产生cookie,并给浏览器设置cookie。设置cookie的目的同上。
  • 具体从用户信息到cookie是通过序列化(serialize)完成的,从cookie到用户信息是通过反序列化(deserialize)完成的。
  • 序列化和反序列化是passport帮我们完成的。分别如下:
// 序列化
passport.serializeUser((user, done) => {
    done(null user.id);
});复制代码
  • 这里传入的参数user正式我们在从数据库取到(创建)一条用户信息后传递给done函数的值。实际上就是数据库中的用户信息。这里的user.id是数据库自动生成的id,而非googleId。原因有两个:1、我们可能会用到不同的验证方法(Facebook、Wechat等),不同系统下采用profile.id无法保证唯一性;2、这里我们使用googleId的唯一作用就是为了授权登陆,登陆后的一切请求都与googleId无关,所以之后请求中携带的cookie信息(正是这次序列化所生成的)应该包含数据库id而非googleId。
passport.deserializeUser((id, done) => {
    User.findById(id).then((user) => {
        done(null, user);
    })
})复制代码
  • 反序列化中id就是cookie信息,也就是数据库产生的id,我们在数据库中根据这个id找到用户信息,以进行进一步操作,最后调用done函数,以完成反序列化。
  • 接下来我们要完成的就是读写cookie的操作。这里我们使用cookie-session这个包,来帮助我们实现对cookie的操作。先看代码,然后解释原理。
  • 注意,这里引入了cookieKey,这其实是我们呢在./config/keys中加入的一段随机字符串(仅字母和数字),用于对cookie信息加密。
// index.js
const passport from 'passport';
const cookieSession from 'cookie-session';

app.use(
    cookieSession({
        maxAge: 7*24*3600*1000,
        keys: [keys.cookieKey]
    )
);
app.use(passport.initialize());
app.use(passport.session());复制代码
  • 至此所有的授权、验证工作已经做完了。cookie-session passport是怎么完成这个工作呢。对于接下来的请求来说,每个请求都会先通过cookie-session,cookie-session从中提取cookie信息、解密然后反序列化,得到一个用户实例。最后把这个用户实例挂在req对象中,然后才把这个req对象传递给实际的route handler。
  • 为了验证上述逻辑是对的,我们新增一个route handler,其中只返回req中挂的user,看其中是否为实例化的model。然后我们先通过localhost:5000/auth/google登陆,然后再访问localhost:5000/api/current_user,查看当前请求所携带的user,不出意外正是googleId为刚才授权的user实例对象。
// ./routes/authRoutes.js
app.get('/api/current_user', (req, res) => {
  res.send(req.user);  
})复制代码
  • 接下来增加一个用于注销用户的api,以方便我们之后的测试。我们之前提到,passport为传递给实际route handler的req对象增加了user,实际上passport还增加了别的东西,其中一个就是logout方法。我们通过调用req.logout(),就可以实现用户的注销登录。
app.get('/api/logout', (req, res) => {
    req.logout();
    res.send(req.user); // logout后应该为undefined
});复制代码
  • 接下来解释几处比较奇怪的代码。
  • 首先是index.js中几处app.use。我们知道express app的作用就是接受请求,并给出响应。app.use中传入的是function,这些function叫做中间件,作用是修改接收的请求,然后再把它传递给实际处理请求的route handler。对于所有请求通用的逻辑比较适合写在中间件中,比如这里的验证用户的逻辑。因为很多请求都需要验证用户的身份才能给出合适的响应,与其在每个route handler都写相同的逻辑(读cookie->解密->反序列化->拿到user model实例),我们把逻辑写在中间件中,所有的请求都会走一遍。这里我们实际用到了两个中间件的逻辑,一个是cookie-session,一个是passport。
  • cookie-session作用是从请求中拿到cookie并解密,那它是如何把解密后的cookie传递给passport的呢?如果我们把/api/current_user中的逻辑改为res.send(req.session),我们会看到一个实际返回的是一个像下面代码所示的对象。这说明此时req.session中存储的是解密之后的cookie信息,实际上是cookie-session把这段解密后的信息挂在了req.session上传递给了passport。然后passport再拿这段信息进行反序列化。
passport: {
    user: "59f893ef4a3dde26c5d9bce2"
}复制代码
  • express官方推荐处理的cookie的库有两个,一个是我们这次用的cookie-session,另一个是express-session,这里主要讲一下二者的区别:就是用户信息存储方式不同。在cookie-session中,cookie就是session,也就是说cookie中包含了session的所有信息。
  • 在express-cookie中,cookie提供对session的引用,具体讲,session是有自己的存储空间(session_store)的,实际要取的数据是从这个存储空间中取的,cookie只提供对这个session的引用(通过session_id)。相比之下后者能存储更多的数据,前者只能存储4KB数据。但是后者可能要设置remote存储,所以更麻烦。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值