Express实例代码分析1——简单的用户验证登录文件
/** * Module dependencies. */ var express = require('../..');// ../..是上级目录的上级目录 var hash = require('pbkdf2-password')() var path = require('path'); var session = require('express-session'); var app = module.exports = express(); // config app.set('view engine', 'ejs');// 定义view引擎,即视图文件后缀名 app.set('views', path.join(__dirname, 'views'));// 定义视图存放的路径,便于node查找 // middleware app.use(express.urlencoded({ extended: false })) app.use(session({ resave: false, // don't save session if unmodified saveUninitialized: false, // don't create session until something stored secret: 'shhhh, very secret' })); /*express-session中间件,session被序列化为json格式,把它当做一个json对象操作就可以了*/ // 保存session消息的中间件 app.use(function(req, res, next){ var err = req.session.error;//取得失败消息 var msg = req.session.success;//取得成功消息 //删除req.session的两个属性 delete req.session.error; delete req.session.success; /*res.locals:An object that contains response local variables scoped to the request, and therefore available only to the view(s) rendered during that request / response cycle (if any). Otherwise, this property is identical to app.locals. This property is useful for exposing request-level information such as the request path name, authenticated user, user settings, and so on.*/ res.locals.message = ''; if (err) res.locals.message = '<p class="msg error">' + err + '</p>'; if (msg) res.locals.message = '<p class="msg success">' + msg + '</p>'; next(); }); //假装是个数据库,实际只有一个用户名为tj var users = { tj: { name: 'tj' } }; // 创建用户时,需要提供用户名、密码、salt、hash hash({ password: 'foobar' }, function (err, pass, salt, hash) { if (err) throw err; // store the salt & hash in the "db" users.tj.salt = salt; users.tj.hash = hash; }); // 认证用户核心代码,需要匹配用户名、hash function authenticate(name, pass, fn) { if (!module.parent) console.log('authenticating %s:%s', name, pass);//如果没有引用本模块的模块,也就是只运行了这个单文件 var user = users[name];//根据name查询用户 if (!user) return fn(new Error('cannot find user')); hash({ password: pass, salt: user.salt }, function (err, pass, salt, hash) { if (err) return fn(err); if (hash === user.hash) return fn(null, user) fn(new Error('invalid password')); }); } /*限制访问中间件,如果浏览器中有session.user,就不限制,否则重定向到login页面*/ function restrict(req, res, next) { if (req.session.user) { next(); } else { req.session.error = 'Access denied!'; res.redirect('/login'); } } //默认网站主页重定向到login app.get('/', function(req, res){ res.redirect('/login'); }); //restricted页面应该是成功登陆后的页面,使用restrict中间件限制访问 app.get('/restricted', restrict, function(req, res){ res.send('Wahoo! restricted area, click to <a href="/logout">logout</a>'); }); /*登出页面,调用session.destory方法(Destroys the session and will unset the req.session property. Once complete, the callback will be invoked. req.session.destroy(function(err) { // cannot access session here }))*/ app.get('/logout', function(req, res){ req.session.destroy(function(){ res.redirect('/'); }); }); //处理用户对login页面的get请求,渲染展示这个页面 app.get('/login', function(req, res){ res.render('login'); }); /*处理用户对login页面的post请求,即通过点击按钮发出而非通过uri的请求*/ app.post('/login', function(req, res){ /*express框架赋予req对象body属性:Contains key-value pairs of data submitted in the request body. By default, it is undefined, and is populated when you use body-parsing middleware such as body-parser and multer.*/ authenticate(req.body.username, req.body.password, function(err, user){ //如果第二个参数不为空,表示登陆成功,设置成功信息 if (user) { /*Session.regenerate(callback): To regenerate the session simply invoke the method. Once complete, a new SID and Session instance will be initialized at req.session and the callback will be invoked.*/ req.session.regenerate(function(){ req.session.user = user; req.session.success = 'Authenticated as ' + user.name + ' click to <a href="/logout">logout</a>. ' + ' You may now access <a href="/restricted">/restricted</a>.'; /*A back redirection redirects the request back to the referer, defaulting to / when the referer is missing. /代表root,back就是返回跳转之前的页面(记录在http请求头里的http referer) res.redirect('back'); */ res.redirect('back'); }); } //如果用户不存在,设置失败信息并重定向到登陆页面 else { req.session.error = 'Authentication failed, please check your ' + ' username and password.' + ' (use "tj" and "foobar")'; res.redirect('/login'); } }); }); /* istanbul ignore next */ if (!module.parent) { app.listen(3000); console.log('Express started on port 3000'); }
总结一下:这是一个基础的用户认证的express实例。真正显示的页面只有login页面和restricted页面,而在views中只有login模板,restricted页面是写在js中的,而这份代码通过redirect和路由比较简洁地处理了用户可能的各种请求和跳转.
关于登陆成功后有一句res.redirect('back'),一开始我以为没什么用,因为在login.ejs模板中,action是/login,也就是这个页面自己,等于说点了登陆按钮,post请求发给了自己。在我注释掉这一句后,发现结果是下面这样的。至于为什么,还不清楚,可能是因为不能post到本页面。这个程序为了简洁,使用get、post两种方法代表用户两种操作,所以需要这种不正常的操作弥补。
这个程序还有一些漏洞,应该在authenticate函数中添加一段没有错误登陆就删除网站session的语句,否则前一个用户正常登陆,因为还是跳转到了login页面,可以第二次登陆,这时候如果输入错误的用户名和密码,尽管没通过验证,仍然可以通过get方法(uri、跳转)访问到restricted页面,不合逻辑。所以这个跳转到本页面有毒,无论从逻辑上还是程序上都有毒,应该直接重定向到restricted页面,不logout就不让出去。
后来我又想了一下,使用重定向还是不靠谱,用户可以正确登陆后,点击返回,回到登录页再错误登陆。我参考了一下csdn的做法,我正确登陆之后,它新建了一个页面,与原来的页面无关了,但这个时候session中肯定有我的信息了,我在登录页随便点了一个广告,果然是以登陆的状态访问的。但是当我错误登陆之后,再在登陆页点击广告,就变成了以未登陆的状态访问。显然,csdn使用了我上面提到的那种做法,一旦错误登陆,就删除这个页面的登陆状态。
总结一下,解决这种用户状态比较稳妥的做法:
1.另外打开一个页面,不让用户使用返回登录页。
2.其实1不重要,只要用户错误登陆一次,就删除session中保存的信息或者更新session中保存的信息,比如从登陆状态改变为未登录状态,就可以解决这种错误逻辑了。