Cookie-Parser是怎樣解析簽名後的cookie的(同一時候對cookie和cookie-signature進行說明)
第一步:我們來學習一下cookie-signature:
var cookie=require('./index'); var val = cookie.sign('hello', 'tobiiscool'); console.log(val); //打印hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI var unsign=cookie.unsign(val, 'tobiiscool') console.log(unsign); //打印hello我們再來看看他在內部是怎樣進行加密的:
exports.sign = function(val, secret){ if ('string' != typeof val) throw new TypeError("Cookie value must be provided as a string."); if ('string' != typeof secret) throw new TypeError("Secret string must be provided."); //最後加密過的字符串是中間包括一個點的字符串,是通過sha256來進行編碼的 return val + '.' + crypto .createHmac('sha256', secret) .update(val) .digest('base64') .replace(/\=+$/, ''); };
那麽我們接下來我們看看怎樣進行解碼:
//也就是說調用unsign方法的時候 var unsign=cookie.unsign(val, 'tobiiscool')首先是獲取到點號前面的明文,然後對這個明文進行一次加密,加密結果是:"明文+sha256編碼的密文" exports.unsign = function(val, secret){ if ('string' != typeof val) throw new TypeError("Signed cookie string must be provided."); if ('string' != typeof secret) throw new TypeError("Secret string must be provided."); //加密後的cookie是這樣的類型的:hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI var str = val.slice(0, val.lastIndexOf('.')) //獲取最後一個點號前面的字符作為我們要解密的cookie的簽名 , mac = exports.sign(str, secret); //然後對傳入的解密的val和剛才通過明文加密的結果進行對象,假設同樣就是true,否則就是false return sha1(mac) == sha1(val) ?我們能夠非常清楚的看到在解密時候首先獲取到明文,也就是點前面的一部分,然後對這部分進行一次加密。這應該是期望的加密的結果。然後我們把期望的加密結果和用戶傳入的加密結果進行一次同樣的sha1算法,比較兩者的結果,假設同樣表示成功!str : false; }; //對string進行sha1算法 function sha1(str){ return crypto.createHash('sha1').update(str).digest('hex'); }
第二步:我們來學習cookie插件
/*! * cookie * Copyright(c) 2012-2014 Roman Shtylman * Copyright(c) 2015 Douglas Christopher Wilson * MIT Licensed */ /** * Module exports. * @public */ exports.parse = parse; exports.serialize = serialize; /** * Module variables. * @private */ var decode = decodeURIComponent; var encode = encodeURIComponent; var pairSplitRegExp = /; */; /** * RegExp to match field-content in RFC 7230 sec 3.2 * * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] * field-vchar = VCHAR / obs-text * obs-text = %x80-FF */ var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/; /** * Parse a cookie header. * * Parse the given cookie header string into an object * The object has the various cookies as keys(names) => values * * @param {string} str * @param {object} [options] * @return {object} * @public */ //cookie.parse(cookies, options);當中cookies參數是從req.headers.cookie中獲取到的! function parse(str, options) { //參數必須是string類型 if (typeof str !== 'string') { throw new TypeError('argument str must be a string'); } var obj = {} var opt = options || {}; //options默認是一個空對象,用戶也能夠自己傳入 var pairs = str.split(pairSplitRegExp); //var pairSplitRegExp = /; */;進行cookie分解 var dec = opt.decode || decode; //dec表示用戶自己傳入的解密cookie的函數,默認是decodeURIComponent pairs.forEach(function(pair) { //接下來遍歷我們通過正則表達式匹配出來的cookie! var eq_idx = pair.indexOf('=') //獲取=號的下標 // skip things that don't look like key=value if (eq_idx < 0) { return; } var key = pair.substr(0, eq_idx).trim() var val = pair.substr(++eq_idx, pair.length).trim(); //獲取cookie的key和value值 // quoted values //假設cookie的值是一個用引號括起來的字符串,這時候就就把前後的引號分割掉! if ('"' == val[0]) { val = val.slice(1, -1); } // only assign once if (undefined == obj[key]) { //通過用戶傳入的options中的decode函數對cookie的值進行解密 obj[key] = tryDecode(val, dec); } }); return obj; } /** * Serialize data into a cookie header. * * Serialize the a name value pair into a cookie string suitable for * http headers. An optional options object specified cookie parameters. * * serialize('foo', 'bar', { httpOnly: true }) * => "foo=bar; httpOnly" * * @param {string} name * @param {string} val * @param {object} [options] * @return {string} * @public */ function serialize(name, val, options) { var opt = options || {}; var enc = opt.encode || encode; if (!fieldContentRegExp.test(name)) { throw new TypeError('argument name is invalid'); } var value = enc(val); if (value && !fieldContentRegExp.test(value)) { throw new TypeError('argument val is invalid'); } var pairs = [name + '=' + value]; if (null != opt.maxAge) { var maxAge = opt.maxAge - 0; if (isNaN(maxAge)) throw new Error('maxAge should be a Number'); pairs.push('Max-Age=' + Math.floor(maxAge)); } if (opt.domain) { if (!fieldContentRegExp.test(opt.domain)) { throw new TypeError('option domain is invalid'); } pairs.push('Domain=' + opt.domain); } if (opt.path) { if (!fieldContentRegExp.test(opt.path)) { throw new TypeError('option path is invalid'); } 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'); if (opt.firstPartyOnly) pairs.push('First-Party-Only'); return pairs.join('; '); } /** * Try decoding a string using a decoding function. * * @param {string} str * @param {function} decode * @private */ function tryDecode(str, decode) { try { //直接調用decode就能夠了 return decode(str); } catch (e) { return str; } }這個插件的功能是比較簡單的,就是把用戶傳入的cookie字符串解析為一個對象,並且options中的decode默認是經過decodeURIComponent處理過的!
最後一步:弄清楚cookie-parser
'use strict'; var cookie = require('cookie'); var signature = require('cookie-signature'); module.exports = cookieParser; module.exports.JSONCookie = JSONCookie; module.exports.JSONCookies = JSONCookies; module.exports.signedCookie = signedCookie; module.exports.signedCookies = signedCookies;非常顯然這個模塊依賴於我們前面引入的cookie和cookie-signature模塊。
我們首先來分析cookieParser方法:
function cookieParser(secret, options) { return function cookieParser(req, res, next) { //假設req.cookies已經存在那麽直接調用以下一個中間件 if (req.cookies) { return next(); } //獲取req.headers.cookie中保存的cookie var cookies = req.headers.cookie; //假設secret不存在。或者存在了是一個數組那麽都會變成一個數組,不存在就是空數組。非常顯然。首先通過引入cookie模塊把req.headers.cookies的字符串解析為一個對象,接著調用signedCookies方法,我們看看signedCookies方法在幹什麽?假設不過一個string那麽secrets就會變成一個只包括這個string的數組 var secrets = !secret || Array.isArray(secret) ? (secret || []) : [secret]; //把第一個參數封裝到req.secret中 req.secret = secrets[0]; //初始化req.cookies為一個空對象 req.cookies = Object.create(null); //初始化req.signedCookies也是一個空對象 req.signedCookies = Object.create(null); //假設請求頭中沒有cookie那麽也用不著對cookie進行加密 // no cookies if (!cookies) { return next(); } //假設請求頭中也存在cookie,那麽把req.headers.cookie中的cookie進行解析,當中options就是傳入的options參數。然後把解析結果封裝到req.cookies上 req.cookies = cookie.parse(cookies, options); // parse signed cookies //解析簽名後的cookie,由於用戶提供了秘鑰把解析的結果封裝到 req.signedCookies上面,傳入的參數是解析出來的cookies並被封裝到req.cookies上面 if (secrets !== 0) { //上面是對cookie進行解析。解析成為鍵值對的形式。這裏是通過secret對cookie進行簽名 req.signedCookies = signedCookies(req.cookies, secrets); req.signedCookies = JSONCookies(req.signedCookies); } // parse JSON cookies req.cookies = JSONCookies(req.cookies); next(); }; }
function signedCookies(obj, secret) { var cookies = Object.keys(obj); //獲取全部的鍵名 var dec; var key; var ret = Object.create(null); var val; //對鍵名進行叠代 for (var i = 0; i < cookies.length; i++) { key = cookies[i]; val = obj[key]; //當中key是cookie的鍵,而val是cookie的值,然後調用signedCookie方法通過secret對cookie進行解密 dec = signedCookie(val, secret); //解密後的cookie怎樣和解密前的cookie是一樣的那麽表示壓根就不須要解密,僅僅有須要被解密的cookie最後才會被返回! if (val !== dec) { ret[key] = dec; delete obj[key]; } } return ret; }這種方法的作用是解析加密後的cookie,然後返回key/value模式。當中調用了signedCookie對每個值進行單獨的解析和推斷:
function signedCookie(str, secret) { //假設要簽名的值不是string那麽直接返回undefined if (typeof str !== 'string') { return undefined; } //假設要簽名的值前兩個不是s:那麽返回原字符串 if (str.substr(0, 2) !== 's:') { return str; } var secrets = !secret || Array.isArray(secret) ?這種方法的作用是返回解密後的cookie的值。傳入的第一個參數str表示的是從client獲取到的加密後的cookie,可是首先要分割掉前面的"s:"。接著調用前面的cookie-signature的unsign方法對cookie的有效性進行推斷,假設返回的不是false表示這個cookie是有效的,這時候返回解密後的cookie就能夠了,所以這時候server端獲取到的就是解密的cookie的值。(secret || []) : [secret]; //把secret轉化為一個數組,假設沒有傳入就是空數組,假設傳入的字符串那麽就變成僅僅有一個元素的數組! for (var i = 0; i < secrets.length; i++) { var val = signature.unsign(str.slice(2), secrets[i]); //對值進行解析,signature.unsign第一個參數是去除前兩個字符也就是"s:"後的值,第二個參數是數組中的每個元素。因此每個元素都會成為秘鑰 //假設解析出來的val值不是false那麽返回解析出來的值,否則返回false。也就是僅僅要有一個解析出來的值不是false那麽後面的秘鑰就不會被使用了! if (val !== false) { return val; } } return false; }
另一點。在signedCookie假設這個cookie是有效的那麽就返回解密後的值。否則就返回false,進而到signedCookies中進行處理,把解密後的cookie封裝到特定的key上面!
接著我們回到上面的這一部分邏輯,弄清楚它在幹什麽?
if (secrets !== 0) { //上面是對cookie進行解析,解析成為鍵值對的形式,這裏是通過secret對cookie進行簽名 req.signedCookies = signedCookies(req.cookies, secrets); req.signedCookies = JSONCookies(req.signedCookies); } // parse JSON cookies req.cookies = JSONCookies(req.cookies); next();我們把解密後的cookie封裝到req.signedCookies域中。接著又對當中的cookie進行了特殊的處理,也就是經過JSONCookies,我們看看這種方法在做什麽?
function JSONCookies(obj) { var cookies = Object.keys(obj); var key; var val; for (var i = 0; i < cookies.length; i++) { key = cookies[i]; //獲取key值 val = JSONCookie(obj[key]); //對每個value值調用JSONCookie方法,假設返回了值那麽更新特定的key相應的value值! if (val) { obj[key] = val; } } return obj; }接著我們看看JSONCookie在幹嗎?
function JSONCookie(str) { if (typeof str !== 'string' || str.substr(0, 2) !== 'j:') { return undefined; } //從這個邏輯非常easy推斷出什麽是JSONCookie(ven圖),也就是必須是值為string同一時候前兩個字符為"j:"才會進行特殊的處理 try { return JSON.parse(str.slice(2)); //否則通過JSON.parse進行處理 } catch (err) { return undefined; } }
總之:假設對簽名的cookie進行了解析後假設是JSONCookie那麽就會進行JSON.parse處理!並且我們通過代碼非常easy看到在req對象上綁定了例如以下的屬性:
req.secret:傳入的秘鑰用於對cookie進行加密
req.cookies:對req.headers.cookie中的cookie進行解析,返回的一個對象
req.signedCookies:保存的是解析後的cookie的真實值。可是可能還會被JSONCookie進行處理
我們再來看看cookie插件中的serialize方法:
//var hdr = cookie.serialize('foo', 'bar'); function serialize(name, val, options) { var opt = options || {}; var enc = opt.encode || encode; //options對象的encode函數 if (!fieldContentRegExp.test(name)) { //var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/; throw new TypeError('argument name is invalid'); } var value = enc(val); //var encode = encodeURIComponent;對value值進行encodeURIComponent操作 if (value && !fieldContentRegExp.test(value)) { throw new TypeError('argument val is invalid'); } //非常顯然沒有對name進行encodeURIComponent。僅僅要name在特定的Unicode區間就能夠了 var pairs = [name + '=' + value]; //指定maxAge if (null != opt.maxAge) { var maxAge = opt.maxAge - 0; if (isNaN(maxAge)) throw new Error('maxAge should be a Number'); pairs.push('Max-Age=' + Math.floor(maxAge)); } //指定domian if (opt.domain) { if (!fieldContentRegExp.test(opt.domain)) { throw new TypeError('option domain is invalid'); } pairs.push('Domain=' + opt.domain); } //指定path if (opt.path) { if (!fieldContentRegExp.test(opt.path)) { throw new TypeError('option path is invalid'); } pairs.push('Path=' + opt.path); } //指定expires if (opt.expires) pairs.push('Expires=' + opt.expires.toUTCString()); if (opt.httpOnly) pairs.push('HttpOnly'); if (opt.secure) pairs.push('Secure'); if (opt.firstPartyOnly) pairs.push('First-Party-Only'); //不知道First-Party-Only是幹嘛的,並且這個頭還不是鍵值對,而是與httponly一樣 return pairs.join('; '); }
註意事項:前面說了要分割掉cookie中的"s:",設置為什麽呢?我們來看看使用了cookie-parser後cookie格式是怎麽樣的?
Cookie:qinliang=s%3ABDOjujVhV0DH9Atax_gl4DgZ4-1RGvjQ.OeUddoRalzB4iSmUHcE8oMziad4Ig7jUT1REzGcYcdg; blog=s%3A-ZkSm8urr8KsXAKsZbSTCp8EWOu7zq2o.Axjo6YmD2dLPGQK9aD1mR8FcpOzyHaGG6cfGUWUVK00我們把前面的qinliang部分的cookie進行一下decodeURIComponent處理:
console.log(decodeURIComponent('s%3ABDOjujVhV0DH9Atax_gl4DgZ4-1RGvjQ.OeUddoRalzB4iSmUHcE8oMziad4Ig7jUT1REzGcYcdg'));這時候就會發現打印例如以下的結果了:
s:BDOjujVhV0DH9Atax_gl4DgZ4-1RGvjQ.OeUddoRalzB4iSmUHcE8oMziad4Ig7jUT1REzGcYcdg這也是為什麽前面說要分割掉前面的"s:"的原因!
當中有兩點須要細致考慮:
(1)cookie的name和value有一個正則表達式範圍
var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;(2)First-Party-Only
if (opt.firstPartyOnly) pairs.push('First-Party-Only');
Cookie-Parser是怎樣解析簽名後的cookie的(同一時候對cookie和cookie-signature進行說明)