Cookie-Parser是如何解析签名后的cookie的(同时对cookie和cookie-signature进行说明)

来源:互联网 发布:外贸b2b软件 编辑:程序博客网 时间:2024/06/03 08:52

第一步:我们来学习一下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(/\=+$/, '');};
通过这部分的代码我们知道加密后的字符串在点签名是明文,后面是经过sha256编码的密文。那么我们接下来我们看看如何进行解码:

//也就是说调用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) ? str : false;};//对string进行sha1算法function sha1(str){  return crypto.createHash('sha1').update(str).digest('hex');}
我们可以很清楚的看到在解密时候首先获取到明文,也就是点前面的一部分,然后对这部分进行一次加密,这应该是期望的加密的结果。然后我们把期望的加密结果和用户传入的加密结果进行一次相同的sha1算法,比较两者的结果,如果相同表示成功!

第二步:我们来学习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不存在,或者存在了是一个数组那么都会变成一个数组,不存在就是空数组。如果仅仅是一个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();  };}
很显然,首先通过引入cookie模块把req.headers.cookies的字符串解析为一个对象,接着调用signedCookies方法,我们看看signedCookies方法在干什么?

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)    ? (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;}
这个方法的作用是返回解密后的cookie的值,传入的第一个参数str表示的是从客户端获取到的加密后的cookie,但是首先要切割掉前面的"s:"。接着调用前面的cookie-signature的unsign方法对cookie的有效性进行判断,如果返回的不是false表示这个cookie是有效的,这时候返回解密后的cookie就可以了,所以这时候服务器端获取到的就是解密的cookie的值。还有一点,在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;  } //从这个逻辑很容易判断出什么是JSONCookie(ven图),也就是必须是值为string同时前两个字符为"j:"才会进行特殊的处理  try {    return JSON.parse(str.slice(2));    //否则通过JSON.parse进行处理  } catch (err) {    return undefined;  }}

总之:如果对签名的cookie进行了解析后如果是JSONCookie那么就会进行JSON.parse处理!而且我们通过代码很容易看到在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');


0 0
原创粉丝点击