从零实现axios(6.1小节-Web安全防御)

186 阅读3分钟

Web 安全防御

axios 里提供了预防 csrf 攻击的功能,我们这一节学习下 axios 是如何预防 csrf 攻击的。首先是在 defaults/index.js文件里的 defaults 配置对象中添加两个字段,其中 xsrfCookieName字段对应的值是 cookie 名,用于从cookie中读取值,xsrfHeaderName字段对应的值是请求头的字段名, 例如:headers['X-XSRF-TOKEN']。详细代码如下:

var defaults = {
  xsrfCookieName: "XSRF-TOKEN",
  xsrfHeaderName: "X-XSRF-TOKEN",
};

在配置对象中添加了这两个配置项之后,接下来就要在请求中携带 token 信息, 这部分的逻辑是在xhr.js中实现的

var utils = require("../utils");
var cookies = require("../helpers/cookies");
var isURLSameOrigin = require("../helpers/isURLSameOrigin");

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // 省略以上部分代码,可以参考前面的内容或最后一章提供的源码资料

    // 取消请求
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }

      reject(createError("Request aborted", config, "ECONNABORTED", request));

      request = null;
    };

    // 添加xsrf请求头
    // config.withCredentials为true,或请求url跟当前域名同源时,才从cookies中读取xsrfValue
    var xsrfValue =
      (config.withCredentials || isURLSameOrigin(fullPath)) &&
      config.xsrfCookieName
        ? cookies.read(config.xsrfCookieName)
        : undefined;

    if (xsrfValue) {
      requestHeaders[config.xsrfHeaderName] = xsrfValue;
    }

    // 省略下面部分代码,可以参考前面的内容或最后一章提供的源码资料
  });
};

config.xsrfCookieName 配置项存在时,如果 config.withCredentials 为 true 时,或者当前 url 路径跟当前域名是同源环境,我们会读取 cookies 中的 config.xsrfCookieName 字段的值。否则,不添加 xsrf 请求头。

我们再看下 isURLSameOrigin 函数的代码,该函数是在 helpers/isURLSameOrigin.js 文件中实现的。我们首先判断当前的环境是不是标准浏览器环境,如果不是,直接返回一个不做任何判断的函数(isURLSameOrigin 函数直接返回 true)。如果是标准浏览器环境,我创建一个 a 标签,通过 a 标签分别来解析当前域名跟请求 url,然后判断它们的 protocolhost 是否相同,如果相同则它们是同源 ur,否则不是。

"use strict";

var utils = require("../utils");
function standardBrowserEnv() {
  var msie = /(msie|trident)/i.test(navigator.userAgent);
  var urlParsingNode = document.createElement("a");
  var originURL;

  /**
   * 解释一个url
   *
   * @param {String} url
   * @returns {Object}
   */
  function resolveURL(url) {
    var href = url;

    if (msie) {
      // IE 浏览器需要设置两次来规范化属性
      urlParsingNode.setAttribute("href", href);
      href = urlParsingNode.href;
    }

    urlParsingNode.setAttribute("href", href);

    // urlParsingNode提供的接口,详情见 - http://url.spec.whatwg.org/#urlutils
    return {
      href: urlParsingNode.href,
      protocol: urlParsingNode.protocol
        ? urlParsingNode.protocol.replace(/:$/, "")
        : "",
      host: urlParsingNode.host,
      search: urlParsingNode.search
        ? urlParsingNode.search.replace(/^\?/, "")
        : "",
      hash: urlParsingNode.hash
        ? urlParsingNode.hash.replace(/^#/, "")
        : "",
      hostname: urlParsingNode.hostname,
      port: urlParsingNode.port,
      pathname:
        urlParsingNode.pathname.charAt(0) === "/"
          ? urlParsingNode.pathname
          : "/" + urlParsingNode.pathname,
    };
  }

  originURL = resolveURL(window.location.href);

  /**
   * 判断请求url跟当前的location是不是同源
   *
   * @param {String} requestURL 要测试的请求url
   * @returns {boolean} 如果同源返回true,否则false。
   */
  return function isURLSameOrigin(requestURL) {
    var parsed = utils.isString(requestURL)
      ? resolveURL(requestURL)
      : requestURL;
    return (
      parsed.protocol === originURL.protocol &&
      parsed.host === originURL.host
    );
  };
};

module.exports = standardBrowserEnv()

我们再看下 cookies 函数的代码, 该函数是在 helpers/cookies.js 文件中实现的。我们只有在标准浏览器环境下才对 cookie 进行处理,代码非常简单,就是导出一个对象,对象里面包含了三个方法,其中 write 方法,用于写入 cookieread 方法用于读取 cookieremove 方法用于删除一个 cookie

"use strict";

var utils = require("../utils");

function standardBrowserEnv() {
  return {
    write: function write(name, value, expires, path, domain, secure) {
      var cookie = [];
      cookie.push(name + "=" + encodeURIComponent(value));

      if (utils.isNumber(expires)) {
        cookie.push("expires=" + new Date(expires).toGMTString());
      }

      if (utils.isString(path)) {
        cookie.push("path=" + path);
      }

      if (utils.isString(domain)) {
        cookie.push("domain=" + domain);
      }

      if (secure === true) {
        cookie.push("secure");
      }

      document.cookie = cookie.join("; ");
    },

    read: function read(name) {
      var match = document.cookie.match(
        new RegExp("(^|;\\s*)(" + name + ")=([^;]*)")
      );
      return match ? decodeURIComponent(match[3]) : null;
    },

    remove: function remove(name) {
      this.write(name, "", Date.now() - 86400000);
    },
  };
}

module.exports = standardBrowserEnv()

除了预防 csrf 攻击外,axios 还提供了 HTTP basic authentication 验证功能。我们可以在请求时在 config 对象中包含 auth 对象,然后把 usernamepassword 通过 btoa 函数转为 base64 编码,通过 Authorization 请求头携带到后端去。

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // 省略这部分代码

    // 构造一个xhr对象
    var request = new XMLHttpRequest();

    // HTTP basic authentication
    if (config.auth) {
      var username = config.auth.username || "";
      var password = config.auth.password
        ? unescape(encodeURIComponent(config.auth.password))
        : "";
      requestHeaders.Authorization = "Basic " + btoa(username + ":" + password);
    }
  });
};