Node - 构建web应用

基础功能

对一个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。

ExpiresMax-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编码加密后在网络中传送,近乎于明文传输。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,607评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,239评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,960评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,750评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,764评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,604评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,347评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,253评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,702评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,893评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,015评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,734评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,352评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,934评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,052评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,216评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,969评论 2 355

推荐阅读更多精彩内容

  • 构建web应用会遇到的问题 请求方法的判断 URL路径的解析 URL中查询字符串的解析 Cookie的解析 表单数...
    TaoGeNet阅读 422评论 0 0
  • 基础功能 之前我们通过http模块创建了一个简单的服务器,但是对于一个网络应用来说肯定是远远不够的,在聚义的业务中...
    exialym阅读 877评论 1 22
  • 数据上传 单纯的头部报文无法携带大量的数据,在业务中,我们往往需要接收一些数据,比如表单提交、文件提交、JSON上...
    Upcccz阅读 314评论 0 0
  • http协议有http0.9,http1.0,http1.1和http2三个版本,但是现在浏览器使用的是htt...
    一现_阅读 1,863评论 0 3
  • 一根K线主要包括三个部分:实体、上影线和下影线。实体即开盘价与收盘价之间的部分;上影线是实体以上的部分,即实体上...
    稻花香摆钟阅读 537评论 0 1