基礎(chǔ)功能
對(duì)一個(gè)web應(yīng)用而言弥喉,具體的業(yè)務(wù)中捷兰,我們可能有如下需求:
1.請(qǐng)求方法的判斷
2.URL的路徑解析
3.URL中查詢字符串解析
4.Cookie的解析
5.Session的使用
6.Basic認(rèn)證
7.表單數(shù)據(jù)的解析
8.任意格式文件的上傳處理
請(qǐng)求方法
客戶端向服務(wù)端發(fā)送報(bào)文饲化,服務(wù)端解析報(bào)文,發(fā)現(xiàn)HTTP請(qǐng)求頭時(shí),調(diào)用http_parser模塊解析請(qǐng)求報(bào)文,并將屬性解析出來定義到ServerRequest對(duì)象上嗤朴,其中請(qǐng)求方法被設(shè)置為req.method。在RESTful類web服務(wù)中請(qǐng)求方法十分重要虫溜,使用PUT雹姊、DELETE、POST衡楞、GET來分別決定對(duì)資源的操作行為吱雏。
路徑解析
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' }
要注意的點(diǎn)是坎背,如果查詢字符串中的鍵出現(xiàn)多次,那么它的值會(huì)是一個(gè)數(shù)組寄雀。
// foo=bar&foo=baz
{
foo: ['bar', 'baz']
}
cookie
HTTP是一個(gè)無狀態(tài)的協(xié)議,現(xiàn)實(shí)中的業(yè)務(wù)卻是需要一定的狀態(tài)的陨献,否則無法區(qū)分用戶之間的身份盒犹,如何標(biāo)識(shí)和認(rèn)證一個(gè)用戶,最早的方案就是cookie了眨业。
cookie的處理分為如下幾步:
1.服務(wù)器向客戶端發(fā)送cookie
2.瀏覽器將cookie保存
3.之后每次瀏覽器都會(huì)將cookie發(fā)向服務(wù)器
http_parser將cookie解析為ServerRequest對(duì)象的req.headers.cookie
急膀。cookie值的格式是:key1=value1;key2=value2
服務(wù)端如何向客戶端發(fā)送cookie,響應(yīng)的cookie值在set-cookie字段中龄捡。
Set-Cookie: name=value; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
path
: 表示這個(gè)cookie影響到的路徑卓嫂,當(dāng)前訪問路徑不滿足時(shí),不發(fā)送這個(gè)cookie聘殖。cookie配置path會(huì)向下傳遞晨雳,配置根路徑行瑞,則所有頁面都會(huì)帶上這個(gè)cookie。
Expires
和Max-Age
:表示cookie的過期時(shí)間餐禁,Expires是格林威治時(shí)間血久,指的是過期時(shí)間,Max-age指這條cookie多久后過期帮非,單位為毫秒
HttpOnly
告知瀏覽器不允許通過腳本document.cookie
去修改cookie的值氧吐,設(shè)置之后,這個(gè)值在document.cookie
中不可見末盔,但是在請(qǐng)求時(shí)依然會(huì)發(fā)送到服務(wù)器筑舅。
Secure
:當(dāng)Secure值為true時(shí),在HTTP中是無效的陨舱,在HTTPS中才有效
Domain
:可以使多個(gè)web服務(wù)器共享cookie翠拣,默認(rèn)是創(chuàng)建cookie的網(wǎng)頁所在的主機(jī)名,不能將一個(gè)cookie的域設(shè)置成服務(wù)器所在域之外的域隅忿。如果a.example.com的頁面創(chuàng)建的cookie把自己的path屬性設(shè)置為“/”心剥,把domain屬性設(shè)置成“.example.com”,那么所有位于a.example.com的網(wǎng)頁和所有位于b.example.com的網(wǎng)頁背桐,以及位于example.com域的其他服務(wù)器上的網(wǎng)頁都可以訪問這個(gè)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('; ');
};
// 服務(wù)器發(fā)送
res.setHeader('Set-Cookie', serialize('isVisit', '1'));
// Set-cookie: isVisit=1;
// 發(fā)送多個(gè)值
res.setHeader('Set-Cookie', [serialize('isVisit', '1'),serialize('user_id', '999')]);
// 這樣在響應(yīng)報(bào)文中會(huì)形成兩條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的大小:如果在根路徑設(shè)置Cookie链峭,那么幾乎所有子路徑都會(huì)帶上這些Cookie畦娄,但并不是所有頁面都需要,所以一定要合理利用Path弊仪。
為靜態(tài)組件使用不同的域名:為不需要Cookie的靜態(tài)組件換個(gè)域名可以減少無效的Cookie的傳輸熙卡,而且主機(jī)名的不同就能增加瀏覽器并行下載的數(shù)量,但是主機(jī)名不是越多越好励饵,因?yàn)槊慷嘁粋€(gè)域名就多一次DNS查詢驳癌。
Cookie除了可以通過后端添加協(xié)議頭的字段設(shè)置外,在前端瀏覽器中也可以通過JavaScript進(jìn)行修改役听,瀏覽器將Cookie通過document.cookie
暴露給JavaScript颓鲜,前端在修改Cookie之后,后續(xù)的網(wǎng)絡(luò)請(qǐng)求中就會(huì)攜帶上修改后的值典予。
Session
通過Cookie甜滨,瀏覽器和服務(wù)器可以實(shí)現(xiàn)狀態(tài)的記錄,但是Cookie是有大小限制瘤袖,而且最大的問題衣摩,是不安全,前后端都能修改捂敌,甚至用戶能直接通過瀏覽器修改Cookie艾扮,為了解決安全問題既琴,Session應(yīng)運(yùn)而生,Session的數(shù)據(jù)只保留在服務(wù)端栏渺,客戶端無法修改呛梆,也無須在協(xié)議中每次都被傳遞。
但是如何將客戶和服務(wù)器中的數(shù)據(jù)一一對(duì)應(yīng)起來磕诊,有兩種實(shí)現(xiàn)方式
基于Cookie來實(shí)現(xiàn)用戶和數(shù)據(jù)的映射
將數(shù)據(jù)放在Session中填物,將口令放在Cookie中,因?yàn)槿绻蛻舳舜鄹牧丝诹罹褪チ擞成潢P(guān)系霎终,也就無法訪問和修改服務(wù)端中的數(shù)據(jù)滞磺,并且session的有效期通常較短,通常為20分鐘莱褒,如果20分鐘客戶端和服務(wù)端沒有交互產(chǎn)生击困,服務(wù)端就會(huì)將數(shù)據(jù)刪除。
一旦服務(wù)器啟用了session广凸,就會(huì)約定一個(gè)鍵作為Session的口令阅茶,比如'session_id'
,一旦服務(wù)器檢查到用戶請(qǐng)求的Cookie中沒有該值谅海,它就會(huì)為之生成一個(gè)值脸哀,且這個(gè)值是唯一且不重復(fù)的,并且設(shè)置超時(shí)時(shí)間扭吁。
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]; // 獲取瀏覽器發(fā)送的cookie有沒有鍵為session_id的
// 沒有就生成一個(gè) session {id: 1573096605319.127, cookie: { expires: 1573097914736} }撞蜂,
// 并存儲(chǔ)在全局的sessions中 sessions { '1573096605319.127': {id: 1573096605319.127, cookie: { expires: 1573097914736} }}
if (!id) {
req.session = generate(res);
} else {
var session = sessions[id]; // 根據(jù)id從全局sessions中取出session
if (session) {
// 如果session存在 查收過期了
if (session.cookie.expires > (new Date()).getTime()) {
// 未過期 設(shè)置新的過期時(shí)間
session.cookie.expires = (new Date()).getTime() + EXPIRES;
// 設(shè)置新的session
req.session = session;
} else {
// 從全局sessions中刪除舊的數(shù)據(jù),重新生成
delete sessions[id];
req.session = generate(res);
}
} else {
// 根據(jù)id沒有取到session侥袜,重新生成
req.session = generate(res);
}
}
handle(req, res);
}
通過查詢字符串來實(shí)現(xiàn)瀏覽器和服務(wù)器端數(shù)據(jù)的對(duì)應(yīng)
檢查請(qǐng)求的查詢字符串蝌诡,如果沒有值,會(huì)先生成新的URL去重定向枫吧;
var redirect = function (url) {
res.setHeader('Location', url);
res.writeHead(302);
res.end();
};
Session與內(nèi)存
如果我們都將Session數(shù)據(jù)存在全局變量中浦旱,即位于內(nèi)存中,這樣將會(huì)帶來隱患九杂,如果用戶增多闽寡,很可能就接觸到了內(nèi)存限制的上限,并且內(nèi)存中的數(shù)據(jù)量加大尼酿,必然會(huì)引起垃圾回收的頻繁掃描,引起性能問題植影。
另一個(gè)問題是我們可能會(huì)為了利用多核CPU而啟動(dòng)多個(gè)進(jìn)程裳擎,用戶請(qǐng)求的連接將可能分配到各個(gè)進(jìn)程中,Node的進(jìn)程與進(jìn)程之間是不能直接共享內(nèi)存的思币,用戶的Session可能會(huì)引起錯(cuò)亂鹿响。
為了解決性能問題和Session數(shù)據(jù)無法跨進(jìn)程共享的問題羡微,常用方案就是將Session集中化,將原本可能分散在多個(gè)進(jìn)程里的數(shù)據(jù)惶我,統(tǒng)一轉(zhuǎn)移到集中的數(shù)據(jù)存儲(chǔ)中妈倔。比如Redis,通過這些高效的緩存绸贡,Node進(jìn)程無須在內(nèi)部維護(hù)數(shù)據(jù)對(duì)象盯蝴,垃圾回收問題和內(nèi)存限制問題可以迎刃而解。
采用第三方緩存來存儲(chǔ)Session會(huì)引起網(wǎng)絡(luò)訪問听怕,相比訪問本地磁盤中的數(shù)據(jù)的速度要慢捧挺,盡管如此,還是會(huì)采用第三方高速存儲(chǔ)尿瞭,是因?yàn)椋?/p>
1.Node與緩存服務(wù)保持長(zhǎng)連接闽烙,握手導(dǎo)致的延遲只影響初始化。
2.高速存儲(chǔ)直接在內(nèi)存中進(jìn)行數(shù)據(jù)存儲(chǔ)和訪問声搁。
3.緩存服務(wù)通常與Node進(jìn)程運(yùn)行在相同的機(jī)器上或者相同的機(jī)房里黑竞,網(wǎng)絡(luò)速度受到的影響較小
// 獲取存儲(chǔ)在緩存中的Session數(shù)據(jù),是異步的疏旨。
// 取
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));
這樣只要不知道私鑰的值很魂,就無法偽造簽名信息,以此實(shí)現(xiàn)對(duì)Session的保護(hù)充石。
緩存
傳統(tǒng)客戶端在安裝后的應(yīng)用過程中僅僅需要傳輸數(shù)據(jù)莫换,Web應(yīng)用還需要傳輸構(gòu)成界面的組件(HTML,CSS,JS文件),這部分內(nèi)容在大多數(shù)場(chǎng)景下并不經(jīng)常變更骤铃,卻需要在每次的應(yīng)用中向客戶端傳遞拉岁,所以對(duì)這一部分需要使用緩存。
{{% notice info %}}
參考:緩存惰爬、ETag
{{% /notice %}}
協(xié)商緩存: If-Modified-Since/Last-Modified 和 If-None-Match/ETag
強(qiáng)制緩存:Expires 和 Cache-Control
Basic認(rèn)證
Basic認(rèn)證是當(dāng)客戶端與服務(wù)端進(jìn)行請(qǐng)求時(shí)喊暖,允許通過用戶名和密碼實(shí)現(xiàn)的一種身份認(rèn)證方式。
如果一個(gè)頁面需要Basic認(rèn)證撕瞧,它會(huì)檢查請(qǐng)求報(bào)文中的Authorization字段陵叽,該字段的值由認(rèn)證方式和加密值構(gòu)成。
<!-- 請(qǐng)求頭 -->
Authorization: Basic dXNlcjpwYXNz
在Basic認(rèn)證中丛版,它會(huì)將用戶和密碼部分組合:username:password
巩掺,然后進(jìn)行Base64編碼。
var encode = function (username, password) {
return new Buffer(username + ':' + password).toString('base64');
};
如果用戶首次訪問該頁面页畦,URL地址中也沒有帶認(rèn)證內(nèi)容胖替,那么瀏覽器會(huì)響應(yīng)一個(gè)401未授權(quán)的狀態(tài)碼。
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)) {
// 響應(yīng)頭中的WWW-Authenticate字段告知瀏覽器采用什么樣的認(rèn)證和加密方式
// 未認(rèn)證會(huì)有交互框彈出
res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"');
res.writeHead(401);
res.end();
} else {
handle(req, res);
}
}
Basic認(rèn)證有太多的缺點(diǎn),使用Base64編碼加密后在網(wǎng)絡(luò)中傳送独令,近乎于明文傳輸端朵。