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。缘由有两个:一、咱们可能会用到不一样的验证方法(Facebook、Wechat等),不一样系统下采用profile.id没法保证惟一性;二、这里咱们使用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存储,因此更麻烦。
