zoukankan      html  css  js  c++  java
  • 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(/=+$/, '');
    };
    
    通过这部分的代码我们知道加密后的字符串在点签名是明文,后面是经过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 = /^[u0009u0020-u007eu0080-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表示的是从client获取到的加密后的cookie,可是首先要分割掉前面的"s:"。接着调用前面的cookie-signature的unsign方法对cookie的有效性进行推断,假设返回的不是false表示这个cookie是有效的,这时候返回解密后的cookie就能够了,所以这时候server端获取到的就是解密的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;
      }
     //从这个逻辑非常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 = /^[u0009u0020-u007eu0080-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 = /^[u0009u0020-u007eu0080-u00ff]+$/;
    (2)First-Party-Only

     if (opt.firstPartyOnly) pairs.push('First-Party-Only');


  • 相关阅读:
    unsupported jsonb version number 123
    如何在MPlayer上支持RTSP
    TDengine 时序数据库的 ADO.Net Core 提供程序 Maikebing.EntityFrameworkCore.Taos
    如何使用IoTSharp对接ModBus?
    如何从源码启动和编译IoTSharp
    Asp.Net Core 自动适应Windows服务、Linux服务、手动启动时的内容路径的扩展方法
    MQTTnet 的Asp.Net Core 认证事件的扩展
    Asp.Net Core 中利用QuartzHostedService 实现 Quartz 注入依赖 (DI)
    The remote certificate is invalid according to the validation procedure 远程证书验证无效
    settings插拔式源码
  • 原文地址:https://www.cnblogs.com/claireyuancy/p/7225276.html
Copyright © 2011-2022 走看看