• 深入理解 Axios


    Axios 是一个基于 Promise 的 HTTP 客户端,同时支持浏览器和 Node.js 环境。拥有以下特性:

    • 支持 Promise API;
    • 能够拦截请求和响应;
    • 能够转换请求和响应数据;
    • 客户端支持防御 CSRF 攻击;
    • 同时支持浏览器和 Node.js 环境;
    • 能够取消请求及自动转换 JSON 数据。

    HTTP 拦截器

    Axios 提供了请求拦截器和响应拦截器来分别处理请求和响应,它们的作用如下:

    • 请求拦截器:在请求发送前统一执行某些操作,比如在请求头中添加 token 字段。
    • 响应拦截器:在接收到服务器响应后统一执行某些操作,比如发现响应状态码为 401 时,自动跳转到登录页。

    Axios 拦截器完整的使用流程:

    1. /** 添加请求拦截器 —— 处理请求配置对象 */
    2. axios.interceptors.request.use(function (config) {
    3. config.headers.token = 'added by interceptor';
    4. return config;
    5. }, function (error) {
    6. /** 对请求错误做些什么 */
    7. return Promise.reject(error);
    8. });
    9. /** 添加响应拦截器 —— 处理响应对象 */
    10. axios.interceptors.response.use(function (data) {
    11. data.data = data.data + ' - modified by interceptor';
    12. return data;
    13. }, function (error) {
    14. /** 超出 2xx 范围的状态码都会触发该函数。对响应错误做点什么 */
    15. return Promise.reject(error);
    16. });
    17. axios({
    18. url: '/hello',
    19. method: 'get',
    20. }).then(res =>{
    21. console.log('axios res.data: ', res.data)
    22. })

    接下来将从任务注册、任务编排和任务调度 三个方面来分析 Axios 拦截器的实现。 

    任务注册

    axios 实例是通过 createInstance 方法创建的,该方法最终返回是 Axios.prototype.request 函数对象。interceptors.request 和 interceptors.response 对象都是 InterceptorManager 类的实例。

    1. /** lib/axios.js */
    2. function createInstance(defaultConfig) {
    3. var context = new Axios(defaultConfig);
    4. var instance = bind(Axios.prototype.request, context);
    5. /** 拷贝 axios.prototype 给 instance */
    6. utils.extend(instance, Axios.prototype, context);
    7. /** 拷贝 context 给 instance */
    8. utils.extend(instance, context);
    9. return instance;
    10. }
    11. /** Create the default instance to be exported */
    12. var axios = createInstance(defaults);
    1. /** lib/core/Axios.js */
    2. function Axios(instanceConfig) {
    3. this.defaults = instanceConfig;
    4. this.interceptors = {
    5. request: new InterceptorManager(),
    6. response: new InterceptorManager()
    7. };
    8. }
    1. /** lib/core/InterceptorManager.js */
    2. function InterceptorManager() {
    3. this.handlers = [];
    4. }
    5. InterceptorManager.prototype.use = function use(fulfilled, rejected) {
    6. this.handlers.push({
    7. fulfilled: fulfilled,
    8. rejected: rejected
    9. });
    10. /** 返回当前的索引,用于移除已注册的拦截器 */
    11. return this.handlers.length - 1;
    12. };

    通过观察 use 方法,我们可知注册的拦截器都会被保存到 InterceptorManager 对象的 handlers 属性中。下面我们用一张图来总结一下 Axios 对象与 InterceptorManager 对象的内部结构与关系:

    任务编排

    注册后,对已注册的任务进行编排,这样才能确保任务的执行顺序。axios 对象对应的 Axios.prototype.request 函数对象的具体实现如下:

    1. /** lib/core/Axios.js */
    2. Axios.prototype.request = function request(config) {
    3. config = mergeConfig(this.defaults, config);
    4. // 省略部分代码
    5. var chain = [dispatchRequest, undefined];
    6. var promise = Promise.resolve(config);
    7. /** 任务编排 */
    8. this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    9. chain.unshift(interceptor.fulfilled, interceptor.rejected);
    10. });
    11. this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    12. chain.push(interceptor.fulfilled, interceptor.rejected);
    13. });
    14. /** 任务调度 */
    15. while (chain.length) {
    16. promise = promise.then(chain.shift(), chain.shift());
    17. }
    18. return promise;
    19. };

    任务编排前后的对比图:

    任务调度

    从 Axios.prototype.request中可以看到,chain 是数组,所以通过 while 语句我们就可以不断地取出设置的任务,然后组装成 Promise 调用链从而实现任务调度,对应的处理流程如下图所示:

    Axios 通过提供拦截器机制,让开发者可以很容易在请求的生命周期中自定义不同的处理逻辑。此外,也可以通过拦截器机制来灵活地扩展 Axios 的功能。综上,axios 具有以下的通用的任务处理模型:

    HTTP 适配器

    Axios 同时支持浏览器和 Node.js 环境,对于浏览器环境来说是通过 XMLHttpRequestfetch API 来发送 HTTP 请求,而对于 Node.js 环境来说是通过 Node.js 内置的 httphttps 模块来发送 HTTP 请求。为了支持不同的环境,Axios 引入了 HTTP 适配器。用于发送 HTTP 请求的 dispatchRequest 方法部分细节如下:

    1. /** lib/core/dispatchRequest.js */
    2. module.exports = function dispatchRequest(config) {
    3. // 省略部分代码
    4. var adapter = config.adapter || defaults.adapter;
    5. return adapter(config).then(function onAdapterResolution(response) {
    6. // 省略部分代码
    7. return response;
    8. }, function onAdapterRejection(reason) {
    9. // 省略部分代码
    10. return Promise.reject(reason);
    11. });
    12. };

    可以看的,Axios 支持自定义适配器,同时也提供了默认的适配器。对于大多数场景,直接使用默认的适配器(包含浏览器和 Node.js 环境的适配代码)即可。默认适配器逻辑为在 getDefaultAdapter 方法中,首先通过平台中特定的对象来区分不同的平台,然后再导入不同的适配器,具体的代码比较简单: 

    1. /** lib/defaults.js */
    2. var defaults = {
    3. adapter: getDefaultAdapter(),
    4. xsrfCookieName: 'XSRF-TOKEN',
    5. xsrfHeaderName: 'X-XSRF-TOKEN',
    6. //...
    7. }
    8. function getDefaultAdapter() {
    9. var adapter;
    10. if (typeof XMLHttpRequest !== 'undefined') {
    11. /** 浏览器 使用 XHR adapter */
    12. adapter = require('./adapters/xhr');
    13. } else if (typeof process !== 'undefined' &&
    14. Object.prototype.toString.call(process) === '[object process]') {
    15. /** node 使用 HTTP adapter */
    16. adapter = require('./adapters/http');
    17. }
    18. return adapter;
    19. }

    至于自定义适配器,参考 Axios 提供的示例:

    1. var settle = require('./../core/settle');
    2. module.exports = function myAdapter(config) {
    3. // 当前时机点:
    4. // - config 配置对象已经与默认的请求配置合并
    5. // - 请求转换器已经运行
    6. // - 请求拦截器已经运行
    7. // 使用提供的config配置对象发起请求
    8. // 根据响应对象处理Promise的状态
    9. return new Promise(function(resolve, reject) {
    10. var response = {
    11. data: responseData,
    12. status: request.status,
    13. statusText: request.statusText,
    14. headers: responseHeaders,
    15. config: config,
    16. request: request
    17. };
    18. settle(resolve, reject, response);
    19. // 此后:
    20. // - 响应转换器将会运行
    21. // - 响应拦截器将会运行
    22. });
    23. }

    当调用自定义适配器之后,需要返回 Promise 对象。这是因为 Axios 内部是通过 Promise 链式调用来完成请求调度的。

    CSRF 防御

    跨站请求伪造(Cross-site request forgery),通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。

    简单来说就是,利用用户的登录被攻击网站在毫不知情情况下点击发起攻击网站 中的指向被攻击网站的链接。跨站请求可以是:图片 URL、超链接、CORS、Form 提交等等。因为,如果只有简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

    CSRF 防御措施一般有:

    1. 检查 Referer 字段(请求来源):在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。因其完全依赖浏览器发送正确的 Referer 字段,无法保证浏览器没有安全漏洞影响到此字段,而且有被篡改的风险。
    2. 同步表单 CSRF 校验:原理是要求所有的用户请求都携带一个 CSRF 攻击者无法获取到的 token,具体是在返回页面时将 token 渲染到页面上,在 form 表单提交的时候通过隐藏域或者作为查询参数把 CSRF token 提交到服务器:
    1. <form method="POST" action="/upload?_csrf={{由服务端生成}}" enctype="multipart/form-data">
    2. 用户名: <input name="name" />
    3. 选择头像: <input name="file" type="file" />
    4. <button type="submit">提交</button>
    5. </form>

            3. 双重 Cookie 防御:将 token 设置在 Cookie 中,在提交(POST、PUT、PATCH、DELETE)等请求时提交 Cookie,并通过请求头或请求体带上 Cookie 中已设置的 token,服务端接收到请求后,再进行对比校验:

    1. let csrfToken = Cookies.get('csrfToken');
    2. function csrfSafeMethod(method) {
    3. /** 以下HTTP方法不需要进行CSRF防护 */
    4. return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
    5. }
    6. $.ajaxSetup({
    7. beforeSend: function(xhr, settings) {
    8. if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
    9. xhr.setRequestHeader('x-csrf-token', csrfToken);
    10. }
    11. },
    12. });

    Axios 中采用的是双重 Cookie 防御 的方案来防御 CSRF 攻击,提供了 xsrfCookieName 和 xsrfHeaderName 两个属性来分别设置 CSRF 的 Cookie 名称和 HTTP 请求头的名称,默认值如下:

    1. /** lib/defaults.js */
    2. var defaults = {
    3. adapter: getDefaultAdapter(),
    4. // 省略部分代码
    5. xsrfCookieName: 'XSRF-TOKEN',
    6. xsrfHeaderName: 'X-XSRF-TOKEN',
    7. };

    以浏览器的适配器为例,Axios 防御 CSRF 攻击:

    1. /** lib/adapters/xhr.js */
    2. module.exports = function xhrAdapter(config) {
    3. return new Promise(function dispatchXhrRequest(resolve, reject) {
    4. var requestHeaders = config.headers;
    5. var request = new XMLHttpRequest();
    6. // 省略部分代码
    7. /** 添加 xsrf 头部 */
    8. if (utils.isStandardBrowserEnv()) {
    9. var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
    10. cookies.read(config.xsrfCookieName) :
    11. undefined;
    12. if (xsrfValue) {
    13. requestHeaders[config.xsrfHeaderName] = xsrfValue;
    14. }
    15. }
    16. request.send(requestData);
    17. });
    18. };

  • 相关阅读:
    Win11自动更新怎么永久关闭?
    【车载以太网测试从入门到精通】——车载以太网休眠唤醒压力测试
    千兆路由只有200M,原来是模式选择不对,也找到了内网不能通过动态域名访问内部服务的原因
    《设计模式》之迭代器模式
    MySQL查询的执行流程
    Java简单使用EasyExcel操作读写excel
    农业新闻查询易语言代码
    jemalloc 5.3.0源码总结
    2023最新短视频配音软件~
    电脑右键新建记事本不见了--设置恢复篇(无需操作注册表)
  • 原文地址:https://blog.csdn.net/qq_42415326/article/details/124911045