n
n
nodebook
搜索文档…
7 Express 进阶
第5章讲了Express的入门知识,这一节趁热打铁要讲一下高级技术。

7.1 使用 session

对于一个网站来说,一个不可避免的问题就是用户登录,这就牵扯到 session 问题了。为此我们需要在app.js中引入两个middleware,cookie-parserexpress-session,上一章的代码6.2.1已经介绍过cookie-parser,接下来重点介绍一下express-session,在app.js中添加如下代码:
1
var cookieParser = require('cookie-parser');
2
var session = require('express-session');
3
var RedisStore = require('connect-redis')(session);
4
var redis = require('redis');
5
6
app.use(cookieParser());//
7
app.use(session({
8
secret: 'GG##@#x27;,
9
cookie:{domain:'localhost'},
10
key:'express_chapter6',
11
resave:false,
12
saveUninitialized:false,
13
store: new RedisStore({
14
client:redis.createClient(6379, '127.0.0.1');,
15
ttl:3600*72,
16
db:2,
17
prefix:'session:chapter6:'
18
})
19
20
}));
Copied!
代码 7.1.1 引入session 其实express默认的session是存储在内存里的,但是这种做法不适合在生产环境使用,首先node如果使用cluster模式的话,内存无法共享,也就是说只能使用单进程;其次,如果在线人数一直增多的话,会造成内存猛增。所以这里的store参数使用了redis,这同样也意味着你需要在你本地(或者远程机器上)启动redis服务,否则程序会报错。 session 函数的 key 参数代表生成cookie的名称。resave参数设置默认为true,代表每次请求结束都会重写store中的数据,不管当前的session有没有被修改。saveUninitialized参数值默认为true,代表将未赋值过的session写入到store,也就是说假设我们的网站需要登录,那么在未登陆之前,也会往store写入数据。所以我们将resavesaveUninitialized都设置为了false。
如果你对 session 原理不是很清楚的话,可以参见我的博文 session的安全性,里面提到了session的基本原理,安全性及攻击防范。
为了减少篇幅,给出的代码都是片段形式:
1
<form method="post" action="/user/login" id="loginForm">
2
<p><input name="username" /><label for="username">用户名</label><p/>
3
<p><input name="password" type="password" /><label for="password">密码</label><p/>
4
<p><input type="submit" value="登陆" /><p/>
5
</form>
6
<script src="//upcdn.b0.upaiyun.com/libs/jquery/jquery-1.10.2.min.js" type="text/javascript"></script>
7
<script type="text/javascript">
8
$(document).ready(function() {
9
$('#loginForm').submit(function() {
10
var $this = $(this);
11
$.ajax({
12
method:$this.attr('method'),
13
url:$this.attr('action'),
14
data:$this.serialize(),
15
dataType:'json'
16
}).done(function(result) {
17
if (result.code == 0) {
18
return location.href = '/user/admin';
19
}
20
alert(result.msg || '服务器异常');
21
}).fail(function() {
22
alert('网络异常');
23
});
24
return false;
25
});
26
});
27
</script>
Copied!
代码7.1.2 登陆前端代码
1
exports.login = function(req, res) {
2
var _body = req.body;
3
var username = _body.username;
4
var password = _body.password;
5
if (username === 'admin' && password === 'admin') {
6
req.session.user = {account:username};
7
return res.send({code:0});
8
}
9
res.send({code:1,msg:'用户名或者密码错误'});
10
}
Copied!
代码7.1.3 登陆后端代码代码7.1.3中通过req.session.user来给session增加一个user的属性,在代码7.1.2中登陆成功后要跳转到/user/admin地址上去,我们接下来看这个地址映射的后端代码:
1
exports.admin = function(req, res) {
2
var user = req.session.user;
3
res.render('user/admin',{user:user});
4
}
Copied!
代码 7.1.4 读取session 通过req.session.user,就可以方便的将之前存储的user属性给读取出来。

7.3 改善我们的登陆

在7.2中,我们已经讲述了怎样使用mongodb了,下面就有机会对于我们7.1中提到的登陆进行改进了。虽然登陆在前端页面登陆仅仅是一个表单,但是在后台处理的流程就不是那么简单。这次我们决定读取数据库中的账号信息进行登陆验证,为此我们创建文件夹models,在其内新建文件user_model.js
1
var crypto = require('crypto');
2
var collection = require('./index');
3
var users = collection.users;
4
5
var passwordValid = exports.passwordValid = function(passwordInput,passwordDb) {
6
return crypto.createHash('sha256').update(passwordInput).digest('base64') === passwordDb;
7
}
8
9
exports.loginCheck = function(username,password,callback) {
10
users.findOne({account:username},function(err,item) {
11
if (err) {
12
console.error('查询用户时失败',err);
13
return callback('查询用户时失败');
14
}
15
if (!item) {
16
return callback('当前用户不存在');
17
}
18
if (!passwordValid(password,item.passwd)) {
19
return callback('用户名或者密码错误');
20
}
21
item.passwd = undefined;
22
callback(false,item);
23
});
24
};
Copied!
代码 7.3.1 登陆验证逻辑
其实单纯用md5/sha1/sha256这些算法来说,都存在被破解的可能性,国内有网站https://cmd5.com 几乎可以破解一切弱密码,解决方案就是使用更长的密码(可以通过用户名和密码进行拼接来计算哈希值),或者使用hmac算法。这里为了演示,使用了最简单的方式。
那么现在控制器中的代码就可以这么写:
1
exports.loginWithDb = function(req, res) {
2
var _body = req.body;
3
var username = _body.username;
4
var password = _body.password;
5
if (!username) {
6
return res.send({code:1,msg:'用户名不能为空'});
7
}
8
userModel.loginCheck(username,password,function(err,item) {
9
if (err) {
10
return res.send({code:1,msg:err});
11
}
12
req.session.user = item;
13
res.send({code:0});
14
});
15
};
Copied!
代码 7.3.2 登陆验证控制器代码 我们在路由器中增加一个链接 /user/login-with-db 指向代码 7.3.2 中的控制器函数,修改代码7.1.2中的表单提交地址即可。

7.4 使用拦截器

之前的章节中介绍过express的middleware,翻译一下就是中间件,下面的内容其实是做一个中间件,但是为啥我给它起名叫拦截器呢,因为我认为对于业务逻辑处理叫拦截器更贴切,因为我理解的middleware仅仅负责解析http数据,不处理业务逻辑。仅仅是个人见解。 之前我们已经做了登陆操作,但是对于一个网站的若干地址(比如说后台地址),不登录是没法用的,我们需要用户在加载这些地址的时候,如果检测到当前处于未登录状态,就统一跳转到登陆页。在每一个控制器中都做一遍登陆状态判断来决定是否跳转,显然是一个笨拙的方法。但是如果使用了拦截器,一切问题就显得简单了。 我们新建文件夹filters,然后在其内新建文件auth_filter.js:
1
const ERROR_NO_LOGIN = 0xffff0000;
2
module.exports = function(req, res, next) {
3
var path = req.path;
4
if (path === '/' || path === '/user/login') {//这些路径不需要做登陆验证
5
return next();
6
}
7
if (req.session && req.session.user) {//已经登陆了
8
return next();
9
}
10
//以下为没有登陆的处理逻辑
11
if (req.xhr) {//当前请求为ajax请求
12
return res.send({code:ERROR_NO_LOGIN,msg:'尚未登陆'});
13
}
14
res.redirect('/');//普通请求
15
};
Copied!
代码 7.4.1 授权拦截器逻辑 同时我们在app.js中引入这段代码:
1
var cookieParser = require('cookie-parser');
2
var bodyParser = require('body-parser');
3
var session = require('express-session');
4
var RedisStore = require('connect-redis')(session);
5
var redis = require('redis');
6
7
var routes = require('./routes/index');
8
var authFilter = require('./filters/auth_filter');
9
10
app.use(bodyParser.json());
11
app.use(bodyParser.urlencoded({ extended: false }));
12
app.use(cookieParser());
13
app.use(express.static(path.join(__dirname, 'public')));
14
15
app.use(session({
16
secret: 'GG##@#x27;,
17
cookie:{domain:'localhost'},
18
key:'express_chapter6',
19
resave:false,
20
saveUninitialized:false,
21
store: new RedisStore({
22
client:redis.createClient(6379, '127.0.0.1'),
23
ttl:3600*72,
24
db:2,
25
prefix:'session:chapter6:'
26
})
27
28
}));
29
app.use(authFilter);
30
app.use('/', routes);
Copied!
代码 7.4.2 引入授权拦截器 将其放到session中间件的后面是由于,我们在这个拦截器中需要读取req.session变量,如果放到session中间件前面,则这个变量不存在。 现在我们在未登录的情况下访问http://localhost:3000/user/admin ,则会直接跳转到登陆页。 本章节代码可以从这里获取:https://github.com/yunnysunny/expressdemo/tree/master/chapter7