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');


  • 相关阅读:
    MSIL实用指南-数据类型转换
    MSIL实用指南-类相关生成
    MSIL实用指南-方法的调用
    MSIL实用指南-struct的生成和操作
    MSIL实用指南-闭包的生成和调用
    Jenkins+maven+gitlab自动化部署之用户权限管理(八)
    Jenkins+maven+gitlab自动化部署之docker发布sprint boot项目(七)
    Centos7部署node
    Jenkins+maven+gitlab自动化部署之前端构建发布(六)
    Jenkins+maven+gitlab自动化部署之构建Java应用(五)
  • 原文地址:https://www.cnblogs.com/claireyuancy/p/7225276.html
Copyright © 2011-2022 走看看