nodejs web应用(一)

请求解析

请求方法在报文的第一行,路径在其后

> GET /path?foo=bar HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: 127.0.0.1:1337 
> Accept: */*

HTTP_Parser在解析时,把报文头的方法取出来设置为req.method,把路径设置为req.url.根据路径进行业务处理的通常有静态文件服务器,选择控制器(/controller/action/a/b/c)

查询字符串:位于路径?之后,可用node提供的querystring处理,也可用url模块,url.parse(req.url,true).query
如果查询字符串的键出现多次,它的值会变成数组

  • 记录状态
  • 服务端先向客户端发送cookie,浏览器保存,之后每次请求都会带上它

构造cookie

1
2
//curl构造cookie字段
curl -v -H "Cookie: foo=bar; baz=val" "http://127.0.0.1:1337/path?foo=bar&foo=baz"

HTTP_Parser会把cookie解析到req.headers.cookie上,格式为key=value;keys=value2

1
2
3
4
5
6
7
8
9
10
11
12
var parseCookie = function(cookie) {
var cookies = {};
if (!cookie) {
return cookies;
}
var list = cookie.split(';');
for (var i = 0; i < list.length; i++) {
var pair = list[i].split('=');
cookies[pair[0].trim()] = pair[1];
}
return cookies;
};

发送cookie通过响应报文:Set-Cookie: name=value; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;

  • path cookie使用的路径,路径不满足不会发送
  • Expires 过期时间 Max-Age 多久后过期 不设置的话关闭浏览器cookie就丢失
  • HttpOnly 不可过脚本document.cookie修改
  • secure 只对https生效
1
2
3
4
5
6
7
8
9
10
11
12
// cookie序列规范化
var serialize = function(name, val, opt) {
var pairs = [name + '=' + encode(val)];
opt = opt || {};
if (opt.maxAge) pairs.push('Max-Age=' + opt.maxAge);
if (opt.domain) pairs.push('Domain=' + opt.domain);
if (opt.path) pairs.push('Path=' + opt.path);
if (opt.expires) pairs.push('Expires=' + opt.expires.toUTCString());
if (opt.httpOnly) pairs.push('HttpOnly');
if (opt.secure) pairs.push('Secure');
return pairs.join('; ');
};

res.setHeader的第二个参数可以是数组以设置多个字段 res.setHeader(‘Set-Cookie’, [serialize(‘foo’, ‘bar’), serialize(‘baz’, ‘val’)]);并且在头部形参两条Set-Cookie

1
2
Set-Cookie: foo=bar; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
Set-Cookie: baz=val; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;

cookie对性能的影响

  • 客户端每次请求都会带上服务端发给它cookie,只要路径匹配
  1. 减少cookie的大小
  2. 为静态组件使用不同的域名 静态文件一般不关心状态,用不到cookie,不同的域名cookie不匹配就不会发送,同时也可突破浏览器下载线程数限制
  3. 减少DNS查询 上面的修改会增加dns查询,但浏览器有dns缓存

session

因为cookie会被前端修改,不安全并且体积大影响性能。而session的数据只保存在服务端,保障数据安全性,也不用每次传递。session会设置有效期且较短,通常20分钟,即此时间内客户端会与服务端交换,session就删掉了

需要解决将用户和服务器的session对应

基于cookie实现用户和数据映射

  • 将口令(session_id)放在cookie与服务端数据映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//生成session
var sessions = {};
var key = 'session_id';
var EXPIRES = 20 * 60 * 1000;
var generate = function() {
var session = {};
session.id = (new Date()).getTime() + Math.random();
session.cookie = {
expire: (new Date()).getTime() + EXPIRES
};
sessions[session.id] = session;
return session;
};
//检查cookie的口令和服务端数据,过期了就重新生成
function(req, res) {
var id = req.cookies[key];
if (!id) {
req.session = generate();
} else {
var session = sessions[id];
if (session) {
if (session.cookie.expire > (new Date()).getTime()) {
//更新超时时间
session.cookie.expire = (new Date()).getTime() + EXPIRES;
req.session = session;
} else {
//超时了,删除数据,重新生成
delete sessions[id];
req.session = generate();
}
} else {
// 如果session过期或口令不对,重新生成
req.session = generate();
}
}
handle(req, res);
}
//将口令注入cookie并响应给客户端
var writeHead = res.writeHead;
res.writeHead = function() {
var cookies = res.getHeader('Set-Cookie');
var session = serialize('Set-Cookie', req.session.id);
cookies = Array.isArray(cookies) ? cookies.concat(session) : [cookies, session]; //array.push()一样
res.setHeader('Set-Cookie', cookies);
return writeHead.apply(this, arguments);
};
  • 通过查询字符串来让客户端和服务端对应(链接带key)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//生成带key的url
var getURL = function(_url, key, value) {
var obj = url.parse(_url, true);
obj.query[key] = value;
return url.format(obj);
};
function(req, res) {
var redirect = function(url) {
res.setHeader('Location', url);
res.writeHead(302);//让浏览器重定向
res.end();
};
var id = req.query[key];
if (!id) {
var session = generate();
redirect(getURL(req.url, key, session.id));
} else {
var session = sessions[id];
if (session) {
if (session.cookie.expire > (new Date()).getTime()) {
//更新超时时间
session.cookie.expire = (new Date()).getTime() + EXPIRES;
req.session = session;
handle(req, res);
} else {
//超时删除旧数据,重新生成
delete sessions[id];
var session = generate();
redirect(getURL(req.url, key, session.id));
}
} else {
//如果session过期或者口令不对,重新生成session
var session = generate(); redirect(getURL(req.url, key, session.id));
}
}
}

session与安全

对session用私钥加密
  • 随机产生口令值可能被命中,并用此套用服务端数据

在服务端设定私钥,用私钥对口令加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 将值通过私钥签名,由.分割原值和签名
var sign = function (val, secret) {
return val + '.' + crypto.createHmac('sha256', secret).update(val).digest('base64').replace(/\=+$/, '');
};
//在响应时,设置session值到cookie或跳转url
var val = sign(req.sessionID, secret);
res.setHeader('Set-Cookie', cookie.serialize(key, val));
//接受请求时取出口令再签名,经行对比
var unsign = function(val, secret) {
var str = val.slice(0, val.lastIndexOf('.'));
return sign(str, secret) == val ? str : false;
};
xxs漏洞
  • 跨站脚本攻击 Access-Control-Allow-Origin

这个前端脚本会获取页面的url,$(‘#box’).html(location.hash.replace(‘#’, ‘’));攻击者可以把脚本藏在链接后http://a.com/pathname#,再对url压缩。用户打开后脚本会执行,攻击者可以用此获取cookie来伪装用户甚至管理员

缓存

  • 让浏览器缓存静态资源
  • 通常只适应于get请求

Smaller icon

一开始本地没有文件,浏览器直接请求服务端并将文件缓存,二次请求时若不能确定是否可用,会发起条件请求,在get中附带If-Modified-Since字段

使用时间戳

1
2
//get报文附带If-Modified-Since/Last-Modified字段
If-Modified-Since: Sun, 03 Feb 2013 06:01:12 GMT

服务端如果没有新版本就只需要响应304,浏览器就会使用本地文件;若更新就发送新文件,让浏览器更新缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var handle = function(req, res) {
fs.stat(filename, function(err, stat) {
var lastModified = stat.mtime.toUTCString();
if (lastModified === req.headers['if-modified-since']) {
res.writeHead(304, "Not Modified");
res.end();
} else {
fs.readFile(filename, function(err, file) {
var lastModified = stat.mtime.toUTCString();
res.setHeader("Last-Modified", lastModified);
res.writeHead(200, "Ok");
res.end(file);
});
}
});
};

缺点:时间戳改动内容不一定改,只能精确到秒,更新频繁的内容可能不生效

ETag(Entity Tag)

  • 服务端确定生成规则生成
  • 请求字段为If-None-Match/ETag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//生成文件hash
var getHash = function(str) {
var shasum = crypto.createHash('sha1');
return shasum.update(str).digest('base64');
};
//请求
var handle = function(req, res) {
fs.readFile(filename, function(err, file) {
var hash = getHash(file);
var noneMatch = req.headers['if-none-match'];
if (hash === noneMatch) {
res.writeHead(304, "Not Modified");
res.end();
} else {
res.setHeader("ETag", hash);
res.writeHead(200, "Ok");
res.end(file);
}
});
};

Expires,Cache-Control

  • 让浏览器明确缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Expires 设置一个过期时间,可能出现时间不同步,文件提前更新等问题
var handle = function(req, res) {
fs.readFile(filename, function(err, file) {
var expires = new Date();
expires.setTime(expires.getTime() + 10 * 365 * 24 * 60 * 60 * 1000);
res.setHeader("Expires", expires.toUTCString());
res.writeHead(200, "Ok");
res.end(file);
});
};
//Cache-Control 可设置更多选项,使用max-age倒计时
var handle = function(req, res) {
fs.readFile(filename, function(err, file) {
res.setHeader("Cache-Control", "max-age=" + 10 * 365 * 24 * 60 * 60 * 1000);
res.writeHead(200, "Ok");
res.end(file);
});
};

http1.0不支持max-age,所以要同时设置Expires,Cache-Control,在浏览器中max-age优先级高于Expires

清除缓存

  • 浏览器是根据url进行缓存的,内容更新时可以让浏览器发起新的url缓存

每次发布在路径中跟随文件内容的hash,http://url.com/?hash=afadfadwe.这样能避免无意义的更新

Basic认证

  • 通过用户名和密码实现的身份认证

当页面需要Basic认证时,会检查请求头部的Authorization字段,它的值由认证方式和加密值构成

$ curl -v "http://user:pass@www.baidu.com/"
> GET / HTTP/1.1
> Authorization: Basic dXNlcjpwYXNz
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5 
> Host: www.baidu.com
> Accept: */*

它会将用户名和密码组合,再Base64编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var encode = function(username, password) {
return new Buffer(username + ':' + password).toString('base64');
};
//每次请求都要带上Authorization,对于没有带认证的请求会返回401
function(req, res) {
var auth = req.headers['authorization'] || '';
var parts = auth.split(' ');
var method = parts[0] || ''; // Basic
var encoded = parts[1] || ''; // dXNlcjpwYXNz,密码字段
var decoded = new Buffer(encoded, 'base64').toString('utf-8').split(":");
var user = decoded[0]; // user
var pass = decoded[1]; // pass
if (!checkUser(user, pass)) {
res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"');
res.writeHead(401);
res.end();
} else {
handle(req, res);
}
}

缺点: 对于密码字段没有加密,只能在https下才能用,但几乎所有浏览器都支持。RFC 2069对它做了改进