Node-3.構建Web應用(一)

NodeJS構建Web應用(一)

基礎功能

對於Web應用而言,在具體的業務中,客戶端和服務器端發送報文,服務器解析報文分析請求頭,我們經常都需要:

  • 判斷請求方法
  • 解析URL的路徑
  • 解析URL上的查詢字符串
  • Cookie的解析
  • Session(會話)的處理
  • Basic認證
  • 解析表單數據
  • 對任意格式的文件上傳處理

請求方法

Web應用中,常見的請求方法是GETPOST,除此之外,還有HEADDELETEPUTDELETE等方法。
請求方法存在報文的第一行的第一個單詞。

GET /path?foo=bar HTTP/1.1

服務器一般只需要處理GET和POST兩類請求, 但是在RESTful類Web服務中請求方法決定資源的操作行爲
PUT代表新建一個資源;
POST表示要更改一個新資源;
GET表示查看一個資源;
DELETE表示要刪除一個資源。
可以通過請求方法來決定響應行爲,如:

function (req, res) {
  switch (req.method) {
  case 'POST':
    update(req, res);
    break;
  case 'DELETE':
    remove(req, res);
    break;
  case 'PUT':
    create(req, res);
    break;
  case 'GET':
  default:
    get(req, res);
  }
}

路徑解析

處理請求有時候需要根據路徑來進行處理。
路徑一般會存在於報文的第一行第二部分

GET /path?foo=bar HTTP/1.1

HTTP_Parser將報文路徑解析爲req.url。一般而言,完整的URL地址是如下這樣的:

http://user:[email protected]:8080/p/a/t/h?query=string#hash

客戶端代理(瀏覽器)會將這個地址解析成報文,將路徑和查詢部分放在報文第一行。需要注意的是,hash部分會被丟棄,不會存在於報文的任何地方。

最常見的根據路徑進行業務處理的應用就是靜態文件服務器,它會根據路徑去查找磁盤中的文件,然後將其響應給客戶端,如下:

function (req, res) {
  var pathname = url.parse(req.url).pathname;
  fs.readFile(path.join(ROOT, pathname), function (err, file) {
    if (err) {
      res.writeHead(404);
      res.end('找不到相關文件。- -');
      return;
    }
    res.writeHead(200);
    res.end(file);
  });
}

另一種比較常見的分發場景是根據路徑來選擇控制器,他將路徑爲控制器和行爲的組合,無需額外配置路由信息,如下:

/user(user的控制器)/addage(行爲是添加)/10(參數)

後臺的會匹配到對應的controller,然後再匹配到對於的控制器行爲,剩餘值作爲參數

查詢字符串

查詢字符串位於路徑之後,在地址欄中路徑後的 “?foo=bar&baz=val” 字符串就是查詢字符串。這個字符串會跟隨在路徑後,形成請求報文首行的第二部分。這部分內容經常需要爲業務邏輯所用,Node提供了 “querystring” 模塊用於處理這部分數據,如下所示:

var url = require('url');
var querystring = require('querystring');
var query = querystring.parse(url.parse(req.url).query);

更簡潔的方法是給url.parse()傳遞第二個參數,如下所示:

var query = url.parse(req.url, true).query;

它會將foo=bar&baz=val解析爲一個JSON對象,如下所示:

{
  foo: 'bar',
  baz: 'val'
}

在業務調用產生之前,我們的中間件或者框架會將查詢字符串轉換,然後掛載在請求對象上供業務使用,如下所示:

function (req, res) {
  req.query = url.parse(req.url, true).query;
  hande(req, res);
}

如果查詢字符串中的鍵出現多次,那麼它的值會是一個數組,如圖:

// foo=bar&foo=baz
var query = url.parse(req.url, true).query;
// {
//   foo: ['bar', 'baz']
// }

注意:
業務的判斷一定要檢查值是數組還是字符串,否則可能出現TypeError異常的情況。

Cookie

Cookie介紹

HTTP是無狀態協議,我們在現實的業務中需要保留一些狀態,否則不能區別用戶的身份。Cookie就可以用來標識和認證一個用戶

Cookie的處理過程爲:

  • 服務器向客戶端發送Cookie
  • 瀏覽器將Cookie保存
  • 之後每次瀏覽器都會將Cookie發向服務器端

客戶端發送的Cookie在請求報文的Cookie字段中,Node的HTTP_Parser會將所有報文字段解析到req.headers上,Cookie就是req.header.cookie,Cookie的值是鍵值對形式的字符串,需要用Cookie某個值的時候需要手動獲取Cookie進行字符串切割處理:

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的性能影響

如果設置了Cookie,在設置的域名下所有的請求都會帶上這些Cookie,從性能優化上考慮,可以:

  • 減小Cookie的大小
    過長的Cookie會消耗性能,所以Cookie不應該存放過多數據,只存必要的數據;
  • 爲靜態組件使用不同的域名
    一些靜態文件是不關心狀態的,Cookie對它而言幾乎是無用的,可以爲不需要Cookie的資源換個域名,可以減少無效的Cookie的傳輸,使得Cookie不再影響靜態資源。而且還可以突破瀏覽器下載線程和數量的限制,因爲域名不同,下載線程數翻倍。但是有一個缺點就是多一個域名需要多一次DNS查詢
  • 減少DNS查詢
    減少DNS查詢和使用不同的域名是衝突的,不過現在的瀏覽器都會進行DNS緩存可以削弱這個副作用。

Cookie可以通過後端添加協議頭的字段設置外,在前端瀏覽器中也可以通過JavaScript進行修改,瀏覽器將document通過document.cookie暴露給了JavaScript。前端在修改Cookie後,後續的網絡請求都會攜帶改後的值。

廣告和在線統計鄰域是最爲依賴Cookie的,通過第三方的廣告或統計腳本,將Cookie和當前頁面綁定,這樣可以標識用戶,得到用戶的瀏覽習慣。廣告商就可以定向投放廣告了。儘管這樣的行爲看起來很可怕,不過Cookie只具有標識性,不能做具有破壞性的事情。

Session

Session與Cookie的區別

Cookie可以在前後端進行修改,因此數據極容易被篡改和僞造Cookie對於敏感數據的保護是無效的
Session的數據只保留在服務器端,客戶無法修改,數據的安全性得到了保障,數據也無須在協議中每次都被傳遞

Session的實現方式

Session如何將每個客戶和服務器中的數據一一對應起來?
常見的有兩種實現方式:

  • 第一種:基於Cookie來實現用戶和數據的映射
    可以將口令放在Cookie中,而不是所有的數據。Cookie中存放口令還是沒有問題的,因爲口令一旦被篡改了,和服務器存在的數據映射關係也會失效。並且Seesion的有效期通常較短,普遍的設置是20分鐘,如果20分鐘內客戶端和服務器端沒有交互產生,服務器就會將數據刪除。由於數據過期時間比較短,且在服務器存儲數據,因此安全性相對比較高

  • 第二種:通過查詢字符串來實現瀏覽器端和服務器端數據的對應
    通過檢查請求的查詢字符串,如果沒有值,會先生成帶值的URL然後再讓瀏覽器重定向跳轉到指定頁面。
    雖然這種方案無需在響應時設置Cookie,但是帶來的風險遠大於基於Cookie實現的風險,因爲只要將地址發給另一個人,他就可以擁有相同的身份。

如何產生口令?
服務器啓動了Session,它將約定一個規則產生唯一值作爲Session的口令,這個值可以隨意約定,比如Connect_uid,Tomcat會採用jssesionid等。
一旦服務器檢查用戶請求Cookie中沒有攜帶這個值,就會爲之生成一個值,並設定超時時間,請求到來時,檢查Cookie中的口令與服務器端的數據,如果過期,就要重新生成,在響應中重新爲客戶端設置新的值。
生成Session的代碼如下:

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;
};

Session與內存的處理

Session數據直接存放在內存中會產生的問題
  • 由於Node存在內存限制,如果用戶增多,就有可能達到內存限制的上限,內存的數據量大會引起頻繁的垃圾回收掃描,引起性能問題。

  • 另一個存在的問題是可能會爲了利用多核CPU而啓動多個進程,用戶請求的連接將可能隨意分配到各個進程中,而Node的進程與進程之間是不能直接共享內存的,可能會引起用戶Session錯亂的問題。

Session集中化

解決性能問題和Session數據無法跨進程共享的問題,常用的方案是將Session集中化 ,將原本可能分散在多個進程中的數據統一到集中的數據存儲中。
常用的工具是Redis、Memcached等,通過這些高效的緩存,Node進程無須在內部維護數據對象,垃圾回收問題和內存限制問題都可以迎刃而解,並且這些高速緩存設計的緩存過期策略更合理更高效,比Node中自行設計的緩存策略更好。

採用第三方緩存來存儲Session引起的一個問題是會引起網絡訪問。理論上來說訪問網絡中的數據要比訪問本地磁盤中的數據速度要慢,因爲涉及到握手、傳輸以及網絡終端自身的磁盤I/O等,儘管如此但依然會採用這些高速緩存的理由有以下幾條:

  • Node與緩存服務保持長連接,而非頻繁的短連接,握手導致的延遲隻影響初始化。
  • 高速緩存直接在內存中進行數據存儲和訪問。
  • 緩存服務通常與Node進程運行在相同的機器上或者相同的機房裏,網絡速度受到的影響較小。
  • 儘管採用專門的緩存服務會比直接在內存中訪問慢,但其影響小之又小,帶來的好處卻遠遠大於直接在Node中保存數據。

Session與安全

雖然Session保存在後端,可以保障安全,但是無論是通過Cookie還是將以查詢字符串的實現方式保存口令,還是存在口令被盜用的情況。如果Web應用的用戶很多,有些簡單的生成口令算法生成的口令值有可能被命中。一旦口令被僞造,服務器端的數據也可能間接被利用。

怎麼可以讓Session更加安全?主要指如何讓口令更加安全。

  • 有一種做法是口令通過私鑰加密進行簽名,使得僞造的成本變高
    比如,將值通過私鑰簽名,由".“分割口令原值和簽名,再設置到Cookie或跳轉URL中,服務端在響應的時候將口令和簽名進行對比,如果簽名非法,就將服務器端的數據立即過期
    如果攻擊者知道”."前的口令,但是不知道私鑰,就不能僞造簽名,從而實現對Session的保護。
    但是如果攻擊者通過某種方式(比如XSS獲得Cookie)獲得了口令和簽名,那他就能實現身份的僞裝。
  • 一種方案是將客戶端的某些獨有信息與口令作爲原值,然後簽名,這樣攻擊者一旦不在原始的客戶端上進行訪問,就會導致簽名失敗。這些獨有信息包括用戶IP和用戶代理(User Agent)。(雖然原始用戶與攻擊者之間也存在上述信息相同的可能性,如局域網出口IP相同,相同的客戶端信息等,不過增加這些考慮還是能夠提高安全性。)

緩存

Web應用需要傳輸構成界面的組件(HTML、JavaScript、CSS文件等)和數據,其中有些靜態資源內容在大多數場景下並不經常變更卻需要在每次的應用中向客戶端傳遞,如果不進行處理,那麼它將造成不必要的帶寬浪費。如果網絡速度較差,就需要花費更多時間來打開頁面,對於用戶的體驗將會造成一定影響。
所以可以讓瀏覽器緩存靜態資源。可以:

  • 添加ExpiresCache-Control 到報文頭中
    對於強緩存(請求頭設置Expires 或Cache-Control),瀏覽器請求時,會對本地文件進行檢查,如果本地已經緩存且沒有過期,會直接使用本地的資源;
  • 配置If-Modified-SinceETags
    對於協商緩存(請求頭設置If-Modified-Since),瀏覽器請求資源時需要發起一次GET條件請求,請求報文中有If-Modified-Since字段,詢問服務器是否有更新版本及服務器文件的最後修改時間,如果沒有新的版本,服務器會返回一個304狀態碼,客戶端就是要本地的資源;如果服務器有新的版本,就將新的內容發送給客戶端,客戶端放棄本地版本。
  • Ajax 可緩存

通常緩存只應用在GET請求中,對於POST、DELETE、PUT這類帶行爲的請求操作不做任何緩存。

Expires和Cache-Control的區別
Expires是一個GMT格式的時間字符串。瀏覽器在接到這個過期值後,只要本地還存在這個緩存文件,在到期時間之前它都不會再發起請求。

Expires的缺陷在於瀏覽器與服務器之間的時間可能不一致,這可能會帶來一些問題,比如文件提前過期,或者到期後並沒有被刪除。

Cache-Control以更豐富的形式,實現相同的功能。可以爲Cache-Control設置了max-age值。

Cache-Control能夠避免瀏覽器端與服務器端時間不同步帶來的不一致性問題。只要進行類似倒計時的方式計算過期時間即可。除此之外,Cache-Control的值還能設置public、private、no-cache、no-store等能夠更精細地控制緩存的選項。

由於在HTTP1.0時還不支持max-age,如今的服務器端在模塊的支持下多半同時對Expires和Cache-Control進行支持。在瀏覽器中如果兩個值同時存在,且被同時支持時,max-age會覆蓋Expires

If-Modified-Since和ETags的區別
If-Modified-Since是一個GMT格式的時間字符串。它向服務器發送條件請求是會附帶If-Modified-Since字段,詢問服務器是否有更新的版本,本地文件的最後修改時間。如果服務器沒有新的版本,會響應304狀態碼,客戶端就使用本地的資源。
但是使用時間戳有些缺陷,可能時間戳發生改變但是文件內容沒有改變;而且時間戳只能精確到秒,更新頻繁的內容無法生效。

HTTP1.1引入ETag(Entity Tag),ETag的請求頭和響應字段是If-None-Match/ETag。ETag由服務器端生成,服務器可以決定他的生成規則,比如將文件內容生成散列值,這樣更加內容生成的標識判斷是否有更新,比起由時間戳判斷更準確。

協商緩存在每次請求前都會向服務器發起一個HTTP請求,服務器響應客戶端資源沒有改動比起重新加載整個頁面代價還是小的多。

清除緩存

  • 在URL路徑上加上Web應用版本號或資源的版本號
  • 路徑上加上文件內容的hash值(更精準)

Basic認證

Basic認證是當客戶端與服務器端進行請求時,允許通過用戶名和密碼實現的一種身份認證方式。
如果一個頁面需要Basic認證,它會檢查請求報文頭中的Authorization字段的內容,該字段的值由認證方式加密值構成,如下所示

$ curl -v "http://user:[email protected]/"
> 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: */*

在Basic認證中,它會將用戶和密碼部分組合:username + “:” + password。然後進行Base64編碼,如下所示:

var encode = function (username, password) {
  return new Buffer(username + ':' + password).toString('base64');
};

如果用戶首次訪問該網頁,URL地址中也沒攜帶認證內容,那麼瀏覽器會響應一個401未授權的狀態碼
WWW-Authenticate字段告知瀏覽器採用什麼樣的認證和加密方式。一般而言,未認證的情況下,瀏覽器會彈出對話框進行交互式提交認證信息,
當認證通過,服務器端響應200狀態碼之後,瀏覽器會保存用戶名和密碼口令,在後續的請求中都攜帶上Authorization信息。
Basic認證有太多的缺點,它雖然經過Base64加密後在網絡中傳送,但是這近乎於明文,十分危險,一般只有在HTTPS的情況下才會使用。
不過Basic認證的支持範圍十分廣泛,幾乎所有的瀏覽器都支持它。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章