请求解析
请求方法在报文的第一行,路径在其后
> 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,浏览器保存,之后每次请求都会带上它
构造cookie
1 2
| 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,只要路径匹配
- 减少cookie的大小
- 为静态组件使用不同的域名 静态文件一般不关心状态,用不到cookie,不同的域名cookie不匹配就不会发送,同时也可突破浏览器下载线程数限制
- 减少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
| 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; }; 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 { req.session = generate(); } } handle(req, res); } 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]; 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
| 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 { 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(/\=+$/, ''); }; 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中附带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
| 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
| 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); }); }; 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'); }; function(req, res) { var auth = req.headers['authorization'] || ''; var parts = auth.split(' '); var method = parts[0] || ''; var encoded = parts[1] || ''; var decoded = new Buffer(encoded, 'base64').toString('utf-8').split(":"); var user = decoded[0]; var pass = decoded[1]; if (!checkUser(user, pass)) { res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"'); res.writeHead(401); res.end(); } else { handle(req, res); } }
|
缺点
: 对于密码字段没有加密,只能在https下才能用,但几乎所有浏览器都支持。RFC 2069对它做了改进