Axios is a Promise-based HTTP client that supports both browsers and node.js environments. It is an excellent HTTP client and is widely used in a large number of Web projects.

As can be seen from the figure above, the Star number of Axios project is 77.9K, and the Fork number is as high as 7.3K, which is a very excellent open source project. So next, Apo will take you to analyze some useful points in Axios project. By the end of this article, you will know the following:

  • Design and implementation of HTTP interceptor;
  • Design and implementation of HTTP adapter;
  • How to defend against CSRF attacks.

Let’s start simple and take a look at Axios.

Follow the full Stack Path to Immortal to read other source code analysis articles and 50 re-learn TS tutorials.

Introduction to Axios

Axios is a PROMISe-based HTTP client with the following features:

  • Support for Promise API;
  • Ability to intercept requests and responses;
  • Ability to transform request and response data;
  • The client defends against CSRF attacks.
  • Supports both browsers and Node.js environments;
  • Ability to cancel requests and automatically convert JSON data.

On the browser side, Axios supports most major browsers, such as Chrome, Firefox, Safari, and IE 11. In addition, Axios has its own ecology:

(Data source — github.com/axios/axios…

After a brief introduction to Axios, let’s take a look at one of the core features it provides: interceptors.

2. Design and implementation of HTTP interceptor

2.1 Introduction to interceptors

For most SPA applications, it is common to use tokens to authenticate the user. This requires that after authentication, we need to carry authentication information on each request. To address this requirement, we can add token information for each request uniformly by encapsulating a uniform request function to avoid processing each request individually.

Later on, if we need to set cache times for some GET requests or control the frequency of calls for some requests, we need to constantly modify the request function to extend its functionality. At this point, our request function would become larger and harder to maintain if we were to consider uniform processing of responses. So how to solve this problem? Axios provides the solution — interceptors.

Axios is an HTTP client based on Promise, and the HTTP protocol is based on request and response:

So Axios provides request interceptors and response interceptors to handle requests and responses, respectively. They do the following:

  • Request interceptor: This interceptor is used to perform certain operations uniformly before the request is sent, such as adding a token field to the request header.
  • Response interceptor: This type of interceptor is used to perform certain actions after receiving a server response, such as automatically jumping to the login page when the response status code is found to be 401.

Set in the Axios interceptor is very simple, through Axios. Interceptors. Request and Axios interceptors. The response object provides the use method, can be set respectively request interceptor and the interceptor:

// Add a request interceptor
axios.interceptors.request.use(function (config) {
  config.headers.token = 'added by interceptor';
  return config;
});

// Add a response interceptor
axios.interceptors.response.use(function (data) {
  data.data = data.data + ' - modified by interceptor';
  return data;
});
Copy the code

So how does an interceptor work? Before looking at the actual code, let’s look at the design idea. Axios is used to send HTTP requests, and request interceptors and response interceptors are essentially a function that implements a specific function.

We can break down sending HTTP requests into different types of subtasks by function, such as a subtask for processing request configuration objects, a subtask for sending HTTP requests, and a subtask for processing response objects. When these subtasks are executed in the order specified, a complete HTTP request is completed.

With that in mind, let’s look at the implementation of the Axios interceptor in terms of task registration, task choreography, and task scheduling.

2.2 Task Registration

From the previous example of using interceptors, we have seen how to register request interceptors and response interceptors, where the request interceptor is used for the subtask that handles the request configuration object and the response interceptor is used for the subtask that handles the response object. To understand how tasks are registered, you need to understand axios and the Axios.Interceptors object.

// lib/axios.js
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);
  // Copy context to instance
  utils.extend(instance, context);
  return instance;
}

// Create the default instance to be exported
var axios = createInstance(defaults);
Copy the code

In the Axios source code, we find the definition of the Axios object, and it’s clear that the default Axios instance is created by the createInstance method, which ultimately returns the Axios.prototype.request function object. At the same time, we found the Axios constructor:

// lib/core/Axios.js
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
Copy the code

In the constructor, we find the definition of the Axios.interceptors object, and we know that the interceptors. Request and interceptors. Response objects are instances of the InterceptorManager class. So next, take a closer look at the InterceptorManager constructor and related use methods to see how tasks are registered:

// lib/core/InterceptorManager.js
function InterceptorManager() {
  this.handlers = [];
}

InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  // Return the current index used to remove the registered interceptors
  return this.handlers.length - 1;
};
Copy the code

By observing the use method, we know that the registered interceptors are saved in the Handlers property of the InterceptorManager object. The following diagram summarizes the internal structure and relationship between the Axios object and the InterceptorManager object:

2.3 Task Orchestration

Now that we know how to register the interceptor tasks, it is not enough to just register the tasks. We also need to orchestrate the registered tasks so that they are executed in order. Here we divide the completion of a complete HTTP request into three phases: processing the request configuration object, initiating the HTTP request, and processing the response object.

Let’s look at how Axios makes a request:

axios({
  url: '/hello'.method: 'get',
}).then(res= >{
  console.log('axios res: ', res)
  console.log('axios res.data: ', res.data)
})
Copy the code

From the previous analysis, we know that the Axios object corresponds to the Axios.prototype.request function object. The implementation of this function is as follows:

// lib/core/Axios.js
Axios.prototype.request = function request(config) {
  config = mergeConfig(this.defaults, config);

  // Omit part of the code
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // Task choreography
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  // Task scheduling
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};
Copy the code

The code for task marshalling is relatively simple. Let’s take a look at the diagram before and after task marshalling:

2.4 Task Scheduling

After the task orchestration is complete, to initiate the HTTP request, we also need to execute the task scheduling in the order after the orchestration. The actual scheduling in Axios is simple, as follows:

 // lib/core/Axios.js
Axios.prototype.request = function request(config) {
  // Omit part of the code
  var promise = Promise.resolve(config);
  while(chain.length) { promise = promise.then(chain.shift(), chain.shift()); }}Copy the code

Since chain is an array, we can continuously fetch the set tasks through the while statement, and then assemble the Promise call chain to realize the task scheduling. The corresponding processing process is shown in the figure below:

Let’s review the complete process of using the Axios interceptor:

// Add a request interceptor that handles the request configuration object
axios.interceptors.request.use(function (config) {
  config.headers.token = 'added by interceptor';
  return config;
});

// Add a response interceptor -- process the response object
axios.interceptors.response.use(function (data) {
  data.data = data.data + ' - modified by interceptor';
  return data;
});

axios({
  url: '/hello'.method: 'get',
}).then(res= >{
  console.log('axios res.data: ', res.data)
})
Copy the code

Having introduced the Axios interceptor, let’s summarize its benefits. By providing an interceptor mechanism, Axios makes it easy for developers to customize different processing logic throughout the life cycle of a request. In addition, the interceptor mechanism can be used to flexibly extend Axios functionality, such as the axios-Respond-Logger and AXIos-debug-log libraries listed in the Axios ecosystem.

Referring to the design model of the Axios interceptor, we can extract the following general task processing model:

Design and implementation of HTTP adapter

3.1 Default HTTP adapter

Axios supports both the browser and Node.js environments. In the browser environment, we can send HTTP requests via the XMLHttpRequest or FETCH API. In the Node.js environment, We can send HTTP requests through the built-in HTTP or HTTPS modules of Node.js.

To support different environments, Axios introduces adapters. In the HTTP interceptor design section, we see a dispatchRequest method, which is used to send HTTP requests. The implementation of this method is as follows:

// lib/core/dispatchRequest.js
module.exports = function dispatchRequest(config) {
  // Omit part of the code
  var adapter = config.adapter || defaults.adapter;
  
  return adapter(config).then(function onAdapterResolution(response) {
    // Omit part of the code
    return response;
  }, function onAdapterRejection(reason) {
    // Omit part of the code
    return Promise.reject(reason);
  });
};
Copy the code

Looking at the dispatchRequest method above, we see that Axios supports custom adapters and also provides a default adapter. For most scenarios, we don’t need a custom adapter, but use the default adapter directly. Therefore, the default adapter contains adaptation code for the browser and node.js environment. The adaptation logic is as follows:

// lib/defaults.js
var defaults = {
  adapter: getDefaultAdapter(),
  xsrfCookieName: 'XSRF-TOKEN'.xsrfHeaderName: 'X-XSRF-TOKEN'./ /...
}

function getDefaultAdapter() {
  var adapter;
  if (typeofXMLHttpRequest ! = ='undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeofprocess ! = ='undefined' && 
    Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}
Copy the code

The getDefaultAdapter method first distinguishes between platforms by platform-specific objects, and then imports the different adapters. The code is relatively simple, so I won’t go into that here.

3.2 Customizing adapters

In addition to the default adapter, we can also customize the adapter. So how do you customize an adapter? Here’s an example from Axios:

var settle = require('. /.. /core/settle');
module.exports = function myAdapter(config) {
  // Current timing:
  // -config The configuration object has been merged with the default request configuration
  // - Request converter already running
  // - Request that the interceptor is running
  
  // Use the provided Config object to initiate the request
  // Handle the state of the Promise based on the response object
  return new Promise(function(resolve, reject) {
    var response = {
      data: responseData,
      status: request.status,
      statusText: request.statusText,
      headers: responseHeaders,
      config: config,
      request: request
    };

    settle(resolve, reject, response);

    / / since then:
    // - The response converter will run
    // - The response interceptor will run
  });
}
Copy the code

In the above examples, we focus on the converter, the runtime point of the interceptor, and the basic requirements of the adapter. For example, when a custom adapter is called, a Promise object needs to be returned. This is because Axios internally schedules requests through a Promise chain call, and those who don’t know can reread the “Interceptor design and Implementation” section.

Now that we know how to customize an adapter, what’s the use of custom adapters? In the Axios ecosystem, Apogo found the Axios-Mock-Adapter library, which makes it easy for developers to simulate requests through custom adapters. The following is an example:

var axios = require("axios");
var MockAdapter = require("axios-mock-adapter");

// Set the mock adapter on the default Axios instance
var mock = new MockAdapter(axios);

// Simulate GET/Users requests
mock.onGet("/users").reply(200, {
  users: [{ id: 1.name: "John Smith"}]}); axios.get("/users").then(function (response) {
  console.log(response.data);
});
Copy the code

If you’re interested in MockAdapter, take a look at the Axios-Mock-Adapter library for yourself. Now that we’ve covered the interceptors and adapters in Axios, here’s a diagram that summarizes the processing of requests in Axios using request interceptors and response interceptors:

4. CSRF defense

4.1 introduction of CSRF

Cross-site Request forgery, often abbreviated to CSRF or XSRF, is an attack that jacks a user into performing an unintended action on the currently logged in Web application.

Cross-site request attacks (XSS) are, in a nutshell, technical attempts by an attacker to trick a user’s browser into visiting a site that the user has authenticated and performing operations (such as sending emails, sending messages, and even property operations such as transferring money and buying goods). Since the browser has been authenticated, the site being visited will run as if it were a real user operation.

In order to better understand the above content, A baoge drew an example diagram of XSS request attack:

In the figure above, the attacker exploits a hole in user authentication on the Web: simple authentication only guarantees that the request is coming from a user’s browser, but it does not guarantee that the request itself is voluntary. Since there are above loopholes, so how should we defend? Let’s look at some common CSRF defenses.

4.2 CSRF Defense Measures

4.2.1 Checking the Referer field

The HTTP header has a Referer field that identifies the address from which the request came. When handling sensitive data requests, the Referer field should generally be under the same domain name as the requested address.

In the example of the mall operation, the Referer field address should normally be the web page address of the mall, which should also be under www.semlinker.com. If the request comes from a CSRF attack, the Referer field will be the address containing the malicious url, not under www.semlinker.com, and the server will be able to identify the malicious access.

This method is simple and easy to implement, only need to add one step verification at the key access. This approach has its limitations, however, as it relies entirely on the browser to send the correct Referer field. Although the HTTP protocol clearly specifies the content of this field, there is no guarantee that the specific implementation of the visiting browser or that the browser has no security vulnerabilities affecting this field. There is also the possibility that attackers will attack some browsers and tamper with the Referer field.

4.2.2 Synchronizing CSRF verification of forms

The CSRF attack succeeds because the server cannot distinguish between a normal request and an attack request. To solve this problem, we can require all user requests to carry a token that the CSRF attacker cannot obtain. For the form attack in the CSRF sample figure, we can use the defense of synchronous form CSRF validation.

Synchronous form CSRF validation is to render the token on the page when returning to the page, and submit the CSRF token to the server by hiding the field or as a query parameter when the form is submitted. For example, when synchronizing a rendered page, add a _csrf query parameter to the form request so that the CSRF token will be submitted when the user submits the form:

<form method="POST" action="/upload? _csrf={{generated by server}}" enctype="multipart/form-data">User name:<input name="name" />Select profile picture:<input name="file" type="file" />
  <button type="submit">submit</button>
</form>
Copy the code
4.2.3 Dual Cookie Defense

Double Cookie defense is to set the token in the Cookie, submit the Cookie when submitting the request (POST, PUT, PATCH, DELETE), and pass the request header or request body with the token set in the Cookie, after the server receives the request, Then the comparison check is performed.

How to set CSRF token in jQuery

let csrfToken = Cookies.get('csrfToken');

function csrfSafeMethod(method) {
  // The following HTTP methods do not require CSRF protection
  return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}

$.ajaxSetup({
  beforeSend: function(xhr, settings) {
    if(! csrfSafeMethod(settings.type) && !this.crossDomain) {
      xhr.setRequestHeader('x-csrf-token', csrfToken); }}});Copy the code

After explaining how and how to defend against CSRF attacks, let’s finish with a look at how Axios defends against CSRF attacks.

4.3 Axios CSRF defense

Axios provides two properties, xsrfCookieName and xsrfHeaderName, to set the CSRF Cookie name and the HTTP request header name, respectively. Their default values are as follows:

// lib/defaults.js
var defaults = {
  adapter: getDefaultAdapter(),

  // Omit part of the code
  xsrfCookieName: 'XSRF-TOKEN'.xsrfHeaderName: 'X-XSRF-TOKEN'};Copy the code

We already know that Axios uses different adapters to send HTTP requests on different platforms. Let’s take a look at how Axios can defend against CSRF attacks using the browser platform as an example:

// lib/adapters/xhr.js
module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestHeaders = config.headers;
    
    var request = new XMLHttpRequest();
    // Omit part of the code
    
    // Add the XSRF header
    if (utils.isStandardBrowserEnv()) {
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;

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

    request.send(requestData);
  });
};
Copy the code

After reading the above code, I believe you already know the answer. It turns out that Axios internally uses a dual Cookie defense scheme to defend against CSRF attacks. Ok, the main content of this article has been introduced, in fact, there are some Axios project we can learn from the place, such as CancelToken design, exception handling mechanism, interested partners can learn.

5. Reference Resources

  • Github – axios
  • Wikipedia – Cross-site request forgery
  • Egg – Security threat CSRF defense