基础功能
对一个web应用而言,具体的业务中,我们可能有如下需求:
1.请求方法的判断
2.URL的路径解析
3.URL中查询字符串解析
4.Cookie的解析
5.Session的使用
6.Basic认证
7.表单数据的解析
8.任意格式文件的上传处理
请求方法
客户端向服务端发送报文,服务端解析报文,发现HTTP请求头时,调用http_parser模块解析请求报文,并将属性解析出来定义到ServerRequest对象上,其中请求方法被设置为req.method。在RESTful类web服务中请求方法十分重要,使用PUT、DELETE、POST、GET来分别决定对资源的操作行为。
路径解析
http_parser将路径解析为req.url,不包括hash部分。
查询字符串
var url = require('url');
var queryString = require('querystring');
var str = 'https://www.iconfont.cn/search/index?q=504&page=3';
console.log(url.parse(str));
// {
// protocol: 'https:',
// slashes: true,
// auth: null,
// host: 'www.iconfont.cn',
// port: null,
// hostname: 'www.iconfont.cn',
// hash: null,
// search: '?q=504&page=3',
// query: 'q=504&page=3',
// pathname: '/search/index',
// path: '/search/index?q=504&page=3',
// href: 'https://www.iconfont.cn/search/index?q=504&page=3'
// }
console.log(url.parse(str).query); // q=504&page=3
console.log(url.parse(str, true).query) //传值true 序列化 -> { q: '504', page: '3' }
console.log(queryString.parse(url.parse(str).query)); // { q: '504', page: '3' }
要注意的点是,如果查询字符串中的键出现多次,那么它的值会是一个数组。
// foo=bar&foo=baz
{
foo: ['bar', 'baz']
}
cookie
HTTP是一个无状态的协议,现实中的业务却是需要一定的状态的,否则无法区分用户之间的身份,如何标识和认证一个用户,最早的方案就是cookie了。
cookie的处理分为如下几步:
1.服务器向客户端发送cookie
2.浏览器将cookie保存
3.之后每次浏览器都会将cookie发向服务器
http_parser将cookie解析为ServerRequest对象的req.headers.cookie
。cookie值的格式是:key1=value1;key2=value2
服务端如何向客户端发送cookie,响应的cookie值在set-cookie字段中。
Set-Cookie: name=value; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
path
: 表示这个cookie影响到的路径,当前访问路径不满足时,不发送这个cookie。cookie配置path会向下传递,配置根路径,则所有页面都会带上这个cookie。
Expires
和Max-Age
:表示cookie的过期时间,Expires是格林威治时间,指的是过期时间,Max-age指这条cookie多久后过期,单位为毫秒
HttpOnly
告知浏览器不允许通过脚本document.cookie
去修改cookie的值,设置之后,这个值在document.cookie
中不可见,但是在请求时依然会发送到服务器。
Secure
:当Secure值为true时,在HTTP中是无效的,在HTTPS中才有效
Domain
:可以使多个web服务器共享cookie,默认是创建cookie的网页所在的主机名,不能将一个cookie的域设置成服务器所在域之外的域。如果a.example.com的页面创建的cookie把自己的path属性设置为“/”,把domain属性设置成“.example.com”,那么所有位于a.example.com的网页和所有位于b.example.com的网页,以及位于example.com域的其他服务器上的网页都可以访问这个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('Set-Cookie', serialize('isVisit', '1'));
// Set-cookie: isVisit=1;
// 发送多个值
res.setHeader('Set-Cookie', [serialize('isVisit', '1'),serialize('user_id', '999')]);
// 这样在响应报文中会形成两条Set-Cookie字段
// Set-Cookie: isVisit=1; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
// Set-Cookie: user_id=999; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com
Cookie的性能影响
减小Cookie的大小:如果在根路径设置Cookie,那么几乎所有子路径都会带上这些Cookie,但并不是所有页面都需要,所以一定要合理利用Path。
为静态组件使用不同的域名:为不需要Cookie的静态组件换个域名可以减少无效的Cookie的传输,而且主机名的不同就能增加浏览器并行下载的数量,但是主机名不是越多越好,因为每多一个域名就多一次DNS查询。
Cookie除了可以通过后端添加协议头的字段设置外,在前端浏览器中也可以通过JavaScript进行修改,浏览器将Cookie通过document.cookie
暴露给JavaScript,前端在修改Cookie之后,后续的网络请求中就会携带上修改后的值。
Session
通过Cookie,浏览器和服务器可以实现状态的记录,但是Cookie是有大小限制,而且最大的问题,是不安全,前后端都能修改,甚至用户能直接通过浏览器修改Cookie,为了解决安全问题,Session应运而生,Session的数据只保留在服务端,客户端无法修改,也无须在协议中每次都被传递。
但是如何将客户和服务器中的数据一一对应起来,有两种实现方式
基于Cookie来实现用户和数据的映射
将数据放在Session中,将口令放在Cookie中,因为如果客户端篡改了口令就失去了映射关系,也就无法访问和修改服务端中的数据,并且session的有效期通常较短,通常为20分钟,如果20分钟客户端和服务端没有交互产生,服务端就会将数据删除。
一旦服务器启用了session,就会约定一个键作为Session的口令,比如'session_id'
,一旦服务器检查到用户请求的Cookie中没有该值,它就会为之生成一个值,且这个值是唯一且不重复的,并且设置超时时间。
var sessions = {};
var key = 'session_id';
var EXPIRES = 20 * 60 * 1000;
var generate = function (res) {
var session = {};
session.id = (new Date()).getTime() + Math.random();
session.cookie = {
expires: (new Date()).getTime() + EXPIRES
};
sessions[session.id] = session;
res.setHeader('Set-Cookie', `${key}=${session.id}`) // session_id=1573096605319.127
return session;
};
function (req, res) {
var id = req.cookies[key]; // 获取浏览器发送的cookie有没有键为session_id的
// 没有就生成一个 session {id: 1573096605319.127, cookie: { expires: 1573097914736} },
// 并存储在全局的sessions中 sessions { '1573096605319.127': {id: 1573096605319.127, cookie: { expires: 1573097914736} }}
if (!id) {
req.session = generate(res);
} else {
var session = sessions[id]; // 根据id从全局sessions中取出session
if (session) {
// 如果session存在 查收过期了
if (session.cookie.expires > (new Date()).getTime()) {
// 未过期 设置新的过期时间
session.cookie.expires = (new Date()).getTime() + EXPIRES;
// 设置新的session
req.session = session;
} else {
// 从全局sessions中删除旧的数据,重新生成
delete sessions[id];
req.session = generate(res);
}
} else {
// 根据id没有取到session,重新生成
req.session = generate(res);
}
}
handle(req, res);
}
通过查询字符串来实现浏览器和服务器端数据的对应
检查请求的查询字符串,如果没有值,会先生成新的URL去重定向;
var redirect = function (url) {
res.setHeader('Location', url);
res.writeHead(302);
res.end();
};
Session与内存
如果我们都将Session数据存在全局变量中,即位于内存中,这样将会带来隐患,如果用户增多,很可能就接触到了内存限制的上限,并且内存中的数据量加大,必然会引起垃圾回收的频繁扫描,引起性能问题。
另一个问题是我们可能会为了利用多核CPU而启动多个进程,用户请求的连接将可能分配到各个进程中,Node的进程与进程之间是不能直接共享内存的,用户的Session可能会引起错乱。
为了解决性能问题和Session数据无法跨进程共享的问题,常用方案就是将Session集中化,将原本可能分散在多个进程里的数据,统一转移到集中的数据存储中。比如Redis,通过这些高效的缓存,Node进程无须在内部维护数据对象,垃圾回收问题和内存限制问题可以迎刃而解。
采用第三方缓存来存储Session会引起网络访问,相比访问本地磁盘中的数据的速度要慢,尽管如此,还是会采用第三方高速存储,是因为:
1.Node与缓存服务保持长连接,握手导致的延迟只影响初始化。
2.高速存储直接在内存中进行数据存储和访问。
3.缓存服务通常与Node进程运行在相同的机器上或者相同的机房里,网络速度受到的影响较小
// 获取存储在缓存中的Session数据,是异步的。
// 取
store.get(id, function(err, data){})
// 保存
store.save(req.session);
Session与安全
将口令的值加密
const crypto = require('crypto');
const SECRET = '1dmpoqjdfpoje1p2dq,w[dk1';
const key = 'session_id';
function sign(str, secret) {
return crypto.createHach('md5')
.update(str + secret)
.digest('base64');
}
// 加入到cookie中
var val = sign(req.sessionID, SECRET);
res.setHeader('Set-Cookie', cookie.serialize(key, val));
这样只要不知道私钥的值,就无法伪造签名信息,以此实现对Session的保护。
缓存
传统客户端在安装后的应用过程中仅仅需要传输数据,Web应用还需要传输构成界面的组件(HTML,CSS,JS文件),这部分内容在大多数场景下并不经常变更,却需要在每次的应用中向客户端传递,所以对这一部分需要使用缓存。
{{% notice info %}}
参考:缓存、ETag
{{% /notice %}}
协商缓存: If-Modified-Since/Last-Modified 和 If-None-Match/ETag
强制缓存:Expires 和 Cache-Control
Basic认证
Basic认证是当客户端与服务端进行请求时,允许通过用户名和密码实现的一种身份认证方式。
如果一个页面需要Basic认证,它会检查请求报文中的Authorization字段,该字段的值由认证方式和加密值构成。
<!-- 请求头 -->
Authorization: Basic dXNlcjpwYXNz
在Basic认证中,它会将用户和密码部分组合:username:password
,然后进行Base64编码。
var encode = function (username, password) {
return new Buffer(username + ':' + password).toString('base64');
};
如果用户首次访问该页面,URL地址中也没有带认证内容,那么浏览器会响应一个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)) {
// 响应头中的WWW-Authenticate字段告知浏览器采用什么样的认证和加密方式
// 未认证会有交互框弹出
res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"');
res.writeHead(401);
res.end();
} else {
handle(req, res);
}
}
Basic认证有太多的缺点,使用Base64编码加密后在网络中传送,近乎于明文传输。