Node - 构建web应用

2019-11-25  本文已影响0人  Upcccz

基础功能

对一个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除了可以通过后端添加协议头的字段设置外,在前端浏览器中也可以通过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编码加密后在网络中传送,近乎于明文传输。

上一篇 下一篇

猜你喜欢

热点阅读