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存储,所以更麻烦。