nodejs web应用(二)

数据上传

node的http模块(HTTP_Parse)只对http头部进行解析,内容部分通过data事件触发,以buffer方式处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var hasBody = function(req) {
return 'transfer-encoding' in req.headers || 'content-length' in req.headers;
};
function(req, res) {
if (hasBody(req)) {
var buffers = [];
req.on('data', function(chunk) {
buffers.push(chunk);
});
req.on('end', function() {
req.rawBody = Buffer.concat(buffers).toString();//buffer.toString()转换成字符串 默认utf-8
handle(req, res);
});
} else {
handle(req, res);
}
}

数据处理

表单数据:Content-Type: application/x-www-form-urlencoded,内容格式和查询字符串相同,foo=bar&baz=val

1
2
3
4
5
6
var handle = function(req, res) {
if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
req.body = querystring.parse(req.rawBody);
}
todo(req, res);
};

json数据:Content-Type: application/json; charset=utf-8 可能附带编码信息

var mime = function(req) {
    var str = req.headers['content-type'] || '';
    return str.split(';')[0];
};
1
2
3
4
5
6
7
8
9
10
11
12
13
var handle = function(req, res) {
if (mime(req) === 'application/json') {
try {
req.body = JSON.parse(req.rawBody);
} catch (e) {
// 异常内容 响应Bad request
res.writeHead(400);
res.end('Invalid JSON');
return;
}
}
todo(req, res);
};

xml数据:Content-Type:application/xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var xml2js = require('xml2js');
var handle = function(req, res) {
if (mime(req) === 'application/xml') {
xml2js.parseString(req.rawBody, function(err, xml) {
if (err) {
// 异常内容 响应Bad request
res.writeHead(400);
res.end('Invalid XML');
return;
}
req.body = xml;
todo(req, res);
});
}
};

附件上传

  • 特殊表单,包含file控件
  • 指定表单属性enctype=”multipart/form-data”

Content-Type: multipart/form-data; boundary=AaB03x Content-Length: 18231
内容可能是由多部分组成,boundary指定每部分内容分界符

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
function(req, res) {
if (hasBody(req)) {
var done = function() {
handle(req, res);
};
if (mime(req) === 'application/json') {
parseJSON(req, done);
} else if (mime(req) === 'application/xml') {
parseXML(req, done);
} else if (mime(req) === 'multipart/form-data') {
parseMultipart(req, done);
}
} else {
handle(req, res);
}
}
//或者使用模块
var formidable = require('formidable');
function(req, res) {
if (hasBody(req)) {
if (mime(req) === 'multipart/form-data') {
var form = new formidable.IncomingForm();
form.parse(req, function(err, fields, files) {
req.body = fields;
req.files = files;
handle(req, res);
});
}
} else {
handle(req, res);
}
}

数据上传与安全

内存限制
  • 限制上传内容大小,超过限制就不接受并发返回400
  • 流式处理,将数据流保存在磁盘,node只保存文件路径

通常node会先保存用户提交的数据,但提交数据过大时,会导致内存占光

Connect对上传数据量的限制方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function(req, res) {
var received = 0,
var len = req.headers['content-length'] ? parseInt(req.headers['content-length'], 10) : null;
// 如果内超过长度限制,返回请求实体过长的状态码
if (len && len > bytes) {
res.writeHead(413);
res.end();
return;
}
// limit
req.on('data', function(chunk) {
received += chunk.length;
if (received > bytes) {
//停止接收数据,触发end()
req.destroy();
}
});
handle(req, res);
};
CSRF
  • Cross-Site Request Forgery 跨站请求伪造

虽然用户通过浏览器访问服务器的Session ID不能被第三方知道,但攻击者可以在另外的网站上构造请求,然后浏览器会根据路由匹配取出cookie并提交到想攻击的服务器

解决方法:添加随机值 在请求时携带

1
2
3
4
5
6
7
8
9
10
11
为每个请求的用户在session中赋予随机值,并在response中告知,然后在请求时返回
function(req, res) {
var token = req.session._csrf || (req.session._csrf = generateRandom(24));
var _csrf = req.body._csrf;
if (token !== _csrf) {
res.writeHead(403);
res.end("禁止访问");
} else {
handle(req, res);
}
}

express的解决方案

路由解析

文件路径型

静态文件: url的路径和网站目录路径一致

动态文件: 服务器根据url路径找到对应的文件,再根据后缀找到脚步的解释器

MVC

  • 控制器 Controller 行为集合,根据url找到对应的控制器和行为
  • 模型 Model 数据操作封装
  • 视图 View 视图渲染,调用视图和相关数据进行页面渲染,输出到客户端
手工映射

需要手工配置路由,但url格式灵活

1
2
3
4
5
6
//完全匹配,配置映射
var routes = [];
var use = function (path, action) { routes.push([path, action]);};
use('/user/setting', exports.setting);
use('/setting/user', exports.setting);
  • 正则匹配

在注册路由时将路径转为一个正则表达式,然后用它来匹配

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
47
48
49
50
51
var pathRegexp = function(path) {
var keys = [];
path = path
.concat(strict ? '' : '/?')
.replace(/\/\(/g, '(?:/')
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?(\*)?/g, function(_, slash, format, key, capture,
optional, star) {
// 将匹配到的键值保存
keys.push(key);
slash = slash || '';
return '' + (optional ? '' : slash) + '(?:'
+ (optional ? slash : '') + (format || '')
+ (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')'
+ (optional || '') + (star ? '(/*)?' : '');
})
.replace(/([\/.])/g, '\\$1')
.replace(/\*/g, '(.*)');
return {
keys: keys,
regexp: new RegExp('^' + path + '$')
};
}
//修改路由注册
var use = function (path, action) { routes.push([pathRegexp(path), action]);};
//匹配
function(req, res) {
var pathname = url.parse(req.url).pathname;
for (var i = 0; i < routes.length; i++) {
var route = routes[i];
// 正则匹配
var reg = route[0].regexp;
var keys = route[0].keys;
var matched = reg.exec(pathname);
if (matched) {
// 将参数内容抽取设置到req.params
var params = {};
for (var i = 0, l = keys.length; i < l; i++) {
var value = matched[i + 1];c
if (value) {
params[keys[i]] = value;
}
}
req.params = params;
var action = route[1];
action(req, res);
return;
}
}
// 处理404请求
handle404(req, res);
}
自然映射
  • 按照约定的方式去查找
1
2
3
4
5
6
/controller/action/param1/param2/param3
exports.setting = function (req, res, month, year) {
// 如路径为/user/setting/12/1987 那么month为12 year为1987
// TODO
};

RESTful

  • Representational State Transfer (资源)表现层状态转化
  • 将服务器提供的内容看做有个资源,每一个URI代表一种资源
  • 客户端通过四个HTTP动词,对服务器端资源进行操作,实现对资源的操作
  • 无状态 客户端和服务器交互的过程中是无状态,即服务器不保存用户状态信息(无session)

对服务器上的资源可以用URI(统一资源定位符)指向它,然后通过URI与资源互动,就是http的四个动作:GET、POST、PUT、DELETE

RESTful架构的状态包含两个,一是应用状态,二是资源状态。应用状态是与某一请求相关的状态信息;资源状态是服务器资源在某一时刻的特定状态,任何请求在同一时刻都会获得这一状态。RESTful的无状态是不保存应用状态,需要由请求方在请求时提高

应用无状态的服务器,可以自由调度请求,更好的实现负载均衡,分布式,并减少宕机带来的风险。