这次的示例基于之前的LoginDemo(见使用cookie保持登录),我们引入MongoDB来保存用户数据。要运行这个示例,前提是MongoDB数据要正常运行(见Node.js开发入门——MongoDB与Mongoose)。示例运行的结果呢,和之前的LoginDemo是一样一样的。因此,我们就只分析引入数据库时项目本身的变化吧。
安装mongoose
命令行环境下导航到LoginDemo目录,执行下面的命令:
npm install mongoose --save
它会自动帮我们安装依赖,也会把mongoose作为依赖项写入项目的package.json文件。
数据库初始化脚本dbInit.js
dbInit.js用来初始化数据库,创建一个名为users的库、一个名为accounts的集合、插入两个账户。代码如下:
var crypto = require('crypto');
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/users');
function hashPW(userName, pwd){
var hash = crypto.createHash('md5');
hash.update(userName + pwd);
return hash.digest('hex');
}
var db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
var count = 2;
function checkClose(){
count = count - 1;
if(count==0) mongoose.disconnect();
}
db.once('open', function() {
console.log('mongoose opened!');
var userSchema = new mongoose.Schema({
name: {type: String, unique: true},
hash: String,
last: String
},
{collection: "accounts"}
);
var User = mongoose.model('accounts', userSchema);
var doc = new User({
name:"admin", hash: hashPW("admin","123456"), last:""
});
doc.save(function(err, doc){
if(err)console.log(err);
else console.log(doc.name + ' saved');
checkClose();
});
doc = new User({
name:"foruok", hash: hashPW("foruok","888888"), last:""
});
doc.save(function(err, doc){
if(err)console.log(err);
else console.log(doc.name + ' saved');
checkClose();
});
});
在启动网站前,执行“node dbInit.js”来做初始化。
在dbInit.js里,我使用了Mongoose的Document对象的save方法保存新建的文档,在“MongoDB与Mongoose”一文中已经用到了,不再啰嗦。
3. 重写users.js
备份一下原来的users.js,新的users.js如下:
var express = require('express');
var router = express.Router();
var crypto = require('crypto');
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/users');
var db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
var User = null;
db.once('open', function() {
console.log('mongoose opened!');
var userSchema = new mongoose.Schema({
name: {type: String, unique: true},
hash: String,
last: String
},
{collection: "accounts"}
);
User = mongoose.model('accounts', userSchema);
});
function hashPW(userName, pwd){
var hash = crypto.createHash('md5');
hash.update(userName + pwd);
return hash.digest('hex');
}
function getLastLoginTime(userName, callback){
if(!User){
callback("");
return;
}
var loginTime = Date().toString();
User.findOne({name:userName}, function(err, doc){
if(err) callback("");
else{
callback(doc.last);
//update login time
doc.update({$set:{last: loginTime}}, function(err, doc){
if(err) console.log("update login time error: " + err);
else console.log("update login time for " + doc.name);
});
}
});
}
function authenticate(userName, hash, callback){
if(!User){ callback(2); return;}
var query = User.findOne().where('name', userName);
query.exec(function(err, doc){
if(err || !doc){ console.log("get user error: " + err); callback(2); return}
if(doc.hash === hash) callback(0);
else callback(1);
});
}
router.requireAuthentication = function(req, res, next){
if(req.path == "/login"){
next();
return;
}
if(req.cookies["account"] != null){
var account = req.cookies["account"];
var user = account.account;
var hash = account.hash;
authenticate(user, hash, function(ret){
if(ret==0){
console.log(req.cookies.account.account + " had logined.");
next();
}else{
console.log("invalid user or pwd, redirect to /login");
res.redirect('/login?'+Date.now());
}
});
}else{
console.log("not login, redirect to /login");
res.redirect('/login?'+Date.now());
}
};
router.post('/login', function(req, res, next){
var userName = req.body.login_username;
var hash = hashPW(userName, req.body.login_password);
console.log("login_username - " + userName + " password - " + req.body.login_password + " hash - " + hash);
authenticate(userName, hash, function(ret){
switch(ret){
case 0: //success
getLastLoginTime(userName, function(lastTime){
console.log("login ok, last - " + lastTime);
res.cookie("account", {account: userName, hash: hash, last: lastTime}, {maxAge: 60000});
//res.cookie("logined", 1, {maxAge: 60000});
res.redirect('/profile?'+Date.now());
console.log("after redirect");
});
break;
case 1: //password error
console.log("password error");
res.render('login', {msg:"密码错误"});
break;
case 2: //user not found
console.log("user not found");
res.render('login', {msg:"用户名不存在"});
break;
}
});
});
router.get('/login', function(req, res, next){
console.log("cookies:");
console.log(req.cookies);
if(req.cookies["account"] != null){
var account = req.cookies["account"];
var user = account.account;
var hash = account.hash;
authenticate(user, hash, function(ret){
if(ret == 0) res.redirect('/profile?'+Date.now());
else res.render('login');
});
}else{
res.render('login');
}
});
router.get('/logout', function(req, res, next){
res.clearCookie("account");
res.redirect('/login?'+Date.now());
});
router.get('/profile', function(req, res, next){
res.render('profile',{
msg:"您登录为:"+req.cookies["account"].account,
title:"登录成功",
lastTime:"上次登录:"+req.cookies["account"].last
});
});
module.exports = router;
代码量和原来差不多,但逻辑复杂了一些。主要是mongoose查询数据库都是异步的,原来我们把账号内置在内存里,查询时是同步的。从同步转到异步,代码发生了翻天覆地的变化,如果你细看一下,会发现,哇哦,到处都是callback和callback的嵌套啊。幸好我是C出身,不然真被搞死了。
为了与Mongoose的异步方式配合,users.js几乎全部重写了。我们以authenticate方法为例讲一下。先看原来的authenticate:
function authenticate(userName, hash){
for(var i = 0; i < userdb.length; ++i){
var user = userdb[i];
if(userName === user.userName){
if(hash === user.hash){
return 0;
}else{
return 1;
}
}
}
return 2;
}
这是典型的同步方式,简单直接,遍历数组比较。再看新的authenticate:
function authenticate(userName, hash, callback){
if(!User){ callback(2); return;}
var query = User.findOne().where('name', userName);
query.exec(function(err, doc){
if(err || !doc){ console.log("get user error: " + err); callback(2); return}
if(doc.hash === hash) callback(0);
else callback(1);
});
}
这是异步的方式了。Node.js是事件驱动的,是一个主线程+多个工作者线程(线程池)的模型,耗时的操作都会投递到线程池里来执行,执行完毕再通过事件通知主线程,主线程处理事件,在适当的时候调用回调。Mongoose处理数据库,也是这种逻辑。
在authenticate里,我使用Model.findOne().where构造了一个Query对象,然后调用Query的exec方法来做查询,而exec提交的查询数据库动作,实际上会在线程池里完成。查询结束后,事件通知主线程,回调我们提供的函数。
现在这种写法,当authenticate()被调用时,很快就返回了,但是查询却被投递到线程池去执行,调用方期望的结果并没有立即到来,依赖调用结果展开的逻辑必须被延宕到回调发生。因此调用方必须改造代码,将部分逻辑放到回调函数里,把回调函数提供给新的authenticate方法。
可以参看requireAuthentication方法的代码,对照下面的同步版本来体会其间异同。
router.requireAuthentication = function(req, res, next){
if(req.path == "/login"){
next();
return;
}
if(req.cookies["account"] != null){
var account = req.cookies["account"];
var user = account.account;
var hash = account.hash;
if(authenticate(user, hash)==0){
console.log(req.cookies.account.account + " had logined.");
next();
return;
}
}
console.log("not login, redirect to /login");
res.redirect('/login?'+Date.now());
};
关于Mongoose操作数据库,CRUD之类的,给两个链接参考下:
其它文章: