axios使用和简单封装

ajax常用插件库axios的使用和简单封装

axios库

  • axios库:框架中使用较多,支持浏览器端、nodejs。在浏览器中axios使用的api就是XMLHttpRequest,是对他的一个封装。
    • 安装/引入axios库后,直接使用axios.get(请求地址)即可发送get请求

请求参数默认自动转为json字符串传输

  • 注意:在网络通信中,请求的数据需要以字符串形式进行传输,所以POST请求才会默认对请求数据进行转换
  • 当使用 Axios 发送 POST 请求时,默认使用 Content-Type: application/json 头部,并将请求数据转换为 JSON 字符串
    • Axios 使用了默认的请求拦截器(request interceptor),在发送请求之前会自动对请求数据进行转换
    • 这适用于大多数情况,特别是当使用 JSON 作为数据格式进行数据交换时
  • headers中的Content-Type被设置为application/x-www-form-urlencoded时,需要将data(请求数据)转换成URLSearchParams对象(即表单数据格式),也就是key1=value1&key2=value2这样的形式,然后再发送HTTP请求。这就需要使用qs.stringify(data)方法进行手动序列化,然后再将序列化后的数据作为请求体data发送
    • 注意:手动序列化后的数据应该是一个字符串,而不是对象。因此在请求时,需要将序列化后的数据直接作为请求体传递给Axios,而不是将其包装在一个对象中。(下面例子中可看到)

简单封装axios

  • 使用场景:在项目中对axios进行封装再使用,可以统一设置token等,还能对请求头行处理,使得调用时可以方便快捷设置Content-Type
  • 可以先回顾一下axios的使用方法
  • axios创建实例以后调用实例和调用axios的方法基本一致(起码常规用到的都有)
  • axios实例的使用可参考文档-请求配置
    • 也是下面get/post封装的参考来源
  • axios返回的数据格式:接口返回的数据将会被包在axios返回的对象的data属性中,可以解构获取下
  • 还可以添加axios的拦截器对响应超时的请求做提示(响应拦截器)、请求拦截器设置token(下方有例子)
  • 一般会在cookie中存一个uid:账号id,通过这个识别不同的用户发出的请求
  • 注意:一般uid由后端Set-Cookie设置,前端不做额外处理,当浏览器收到带有 Set-Cookie 头的 HTTP 响应时,它会将该 Cookie 存储在浏览器中。然后,对于同一域名和路径的后续请求,浏览器会自动在请求头中添加相应的 Cookie 信息
    • 前提:响应中的 Set-Cookie 头设置了有效的 Cookie 值和属性,例如名称、值、过期时间、路径等。

设置请求头

  • 补充了解:
    • withCredentials: true:当发送跨域请求时,默认情况下,浏览器会阻止发送跨域请求的请求头中携带凭据信息,例如 cookie。这是出于安全性的考虑,以防止潜在的安全漏洞。然而,有些情况下,我们需要在跨域请求中携带凭据,这时就可以使用 withCredentials 参数来设置
    • 《HTTP报头Accept与Content-Type的区别》
    • axios配置请求头content-type可以使用接口时配置,也可以全局配置,还可以像下面这样对axios实例进行配置
  • 一个简单封装常用的get和post方法,可以在调用时简单设置请求头的content-type的例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    // fs-axios.js
    import qs from 'qs';
    import axios from 'axios';
    import { apiDomain } from '~/configs/env'; // apiDomain指向process.env.API_DOMAIN设置的是/api

    const DkAxios = axios.create({
    withCredentials: true, // 启用跨域请求时携带凭据(如 cookie、HTTP 认证等)的功能
    baseURL: apiDomain,
    timeout: 60000, // 设置超时时间
    });

    export const FSAxios = {
    get(url, data) {
    return DkAxios({
    url,
    // headers: { 'Content-Type': 'application/json' }, get不需要设置,毕竟get请求体是空的,通过?键值对传参
    method: 'GET',
    params: data, // 注意:params是与请求一起发送的 URL 参数,一般给get请求用
    });
    },
    post(url, data = {}, type, { header, baseURL = apiDomain } = {}) {
    let headers;
    if (type === 'json') {
    headers = { 'Content-Type': 'application/json' };
    } else {
    headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
    data = qs.stringify(data); // form类型的data传字符串,json类型的data传对象
    }
    if (header) {
    headers = {
    ...headers,
    ...header,
    }
    }
    DkAxios.defaults.withCredentials = false

    DkAxios.defaults.baseURL = baseURL

    return DkAxios({
    url,
    headers,
    method: 'POST',
    data, // data是作为请求体被发送的数据,给post等请求用
    });
    },
    }
1
2
3
4
5
6
7
8
9
// 使用例子,也可以全局引用一下
import { FSAxios } from "~/jslibs/fs-axios";

// 接口返回的数据会被包在data对象里,可以解构取一下,这样res就是接口返回的数据了
const { data: res } = await FSAxios.get("/services/logisticsMonitoring/queryIsShowAuthorizationPage");
// 'Content-Type': 'application/x-www-form-urlencoded'的post请求
const { data: res } = await FSAxios.post("/services/logisticsMonitoring/authorization", { isCheck: true });
// 'Content-Type': 'application/json'的post请求
const { data: res } = await FSAxios.post("/services/logisticsMonitoring/authorization", { isCheck: true }, 'json');

区分qs.stringify和JSON.stringify

  • Axios的默认行为是将请求参数转换为JSON字符串(当Content-Type设置为application/json时),而不是URL编码的字符串。如果你希望将请求参数以application/x-www-form-urlencoded格式发送,你需要使用qs.stringify方法对参数进行转换
  • 设置headers = { 'Content-Type': 'application/x-www-form-urlencoded' }时我们对传参特殊处理qs.stringify(params),这里和Content-Type: application/json
1
2
3
4
5
6
7
8
9
10
// 两种stringify的区别:
const params = { name: 'John', age: 30 };
const encodedParams = qs.stringify(params);
console.log(encodedParams);
// 输出:name=John&age=30

const data = { name: 'John', age: 30 };
const jsonData = JSON.stringify(data);
console.log(jsonData);
// 输出:{"name":"John","age":30}
  • qs.stringify : qs 库中的一个方法,用于将 JavaScript 对象转换为 URL 编码形式的查询字符串。它主要用于将对象序列化为符合 application/x-www-form-urlencoded 数据格式的字符串。这种格式要求将特殊字符进行编码,如空格转换为 %20、加号转换为 %2B,以便正确地传递表单数据
  • JSON.stringify : JavaScript 内置的方法,用于将 JavaScript 对象或值转换为 JSON 字符串。它主要用于在客户端和服务器之间传递结构化数据,并在数据交换和存储中广泛使用

拦截器设置token

  • CSRF(Cross-Site Request Forgery)是一种常见的网络安全攻击,攻击者利用用户在已认证的网站上的身份来伪造请求,以执行未经授权的操作。为了防止这种攻击,可以使用 CSRF Token 技术。
    • CSRF Token 是一个随机生成的令牌,用于验证请求的来源是否可信。它在用户进行认证时由服务器生成,并嵌入到前端页面中的表单或请求中。
    • 由于攻击者无法获取到合法用户的 CSRF Token,因此无法在受害者不知情的情况下伪造包含正确 CSRF Token 的请求,从而无法成功执行攻击。
    • 服务器会验证请求中的 CSRF Token 是否与服务器生成的匹配,以确认请求的来源是合法的。如果 CSRF Token 不匹配,服务器会拒绝请求。
    • 通过使用 CSRF Token,网站可以有效地防止 CSRF 攻击,提高应用的安全性。重要的是,在实施 CSRF Token 时,需要确保生成的 Token 是随机且具有足够的复杂性,以增加攻击者猜测或伪造 Token 的难度。
  • 在dk-axios.js中通过拦截器写入csrf-token:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // fs-axios.js
    import axios from 'axios';
    import { apiDomain } from '~/configs/env'; // apiDomain指向process.env.API_DOMAIN设置的是/api
    import { encrypt } from '@/utils/crypto.js' // 用CryptoJS加密库

    const DkAxios = axios.create({
    withCredentials: true, // 启用跨域请求时携带凭据(如 cookie、HTTP 认证等)的功能
    baseURL: apiDomain,
    timeout: 60000, // 设置超时时间
    });

    function verifyOriginal(url) {
    let flag = true
    if (url.indexOf('http') > -1 || url.indexOf('https') > -1) {
    flag = url.indexOf(window.origin) > -1
    }
    return flag
    }

    // 添加请求拦截器
    DkAxios.interceptors.request.use(
    // 在发送请求之前
    (config) => { // config是包含请求配置的对象
    const { url } = config // 用后端返回的值来定token
    if (verifyOriginal(url)) {
    const time = new Date().getTime()
    const csrfToken = `${url}___${time}`
    config.headers['csrf-token'] = encrypt(csrfToken)
    }
    return config; // 拦截器函数需要返回 config 对象或一个 Promise 对象,用于后续请求的继续处理
    },
    // 处理请求错误
    error => Promise.reject(error),
    );

    // ...其他处理...

    export default DkAxios; // 可以像上面一样对get post请求继续进行处理再抛出

使用url做底生成token

  • 一般来说 Token 令牌的生成应由后端来负责。 这是因为 Token 令牌通常用于验证用户身份或授权访问,这些操作涉及到安全性和权限控制,应该由后端来管理和控制。前端只负责将 Token 令牌添加到请求中,并在需要的情况下将其传递给后端进行验证和授权。但是我们系统目前是前端自己生成的
  • 使用 URL 来加密生成 Token 是一种常见的做法,主要基于以下几点原因:
    • 唯一性:URL 在每个请求中都是唯一的,因为它包含了特定的路径和查询参数。因此,使用 URL 来生成 Token 可以确保每个请求的 Token 都是唯一的,从而增加了安全性。
    • 不可预测性:URL 通常包含随机性较高的字符、数字和特殊符号,例如路径参数、查询参数等。通过将 URL 进行哈希或加密,可以将这些字符转换为固定长度的不可预测的字符串,从而生成不易被猜测的 Token。
    • 无需服务器存储:使用 URL 来生成 Token 的好处之一是无需在服务器端存储 Token。服务器只需要在每个请求中验证 Token 的有效性即可,而无需维护 Token 的状态或存储在数据库中。
  • 目前使用url做底生成token这种做法,后端接收到token进行解密后,会验证请求中path(也就是前端请求的url部分,有可能不包含域名、协议或查询参数,后端不同框架处理方式不同)与前端所传token中url的关系,以及时间差是否超过3秒(前端发出的时间和后端收到的时间差),两个条件有一个不符合就算无效请求
  • 如果是攻击链接,则后端获取到的path和前端处理token时使用的url可能不同,验证时无法通过,达到阻挡攻击的效果
  • 这种方式是不可靠的,因为攻击者可能仍然能够构造有效的恶意请求,可能导致这种方式不可靠的原因:
    • 可预测性问题:如果仅仅通过加密的 URL 和时间戳来判断请求是否合法,那么攻击者仍然可以通过分析和猜测 URL 和时间戳的规律来构造有效的恶意请求。攻击者可以尝试不同的 URL 和时间戳组合,以绕过这种简单的检查。
    • 反向工程问题:如果加密算法或密钥在前端可见或容易被猜测,攻击者可能能够解密 Token 并构造伪造的请求。因此,对于安全性要求较高的应用程序,将密钥和加密算法保持在后端,并使用安全的加密方案是更可靠的做法。
    • 不完全的防御:CSRF 攻击并不仅仅取决于请求的 URL,还可能受到其他因素的影响,如请求方法、请求头、Cookie 等。仅仅通过检查 URL 是否匹配来判断请求的合法性可能无法提供全面的防护。
  • 对于有效的 CSRF 防御,建议采用更为可靠的方法
    • 使用 CSRF Token:后端生成一个随机的 CSRF Token 并将其包含在每个请求的表单参数或请求头中。前端在提交请求时将 Token 一并发送给后端,后端验证 Token 的有效性来确保请求的合法性。

token正确使用流程

  1. 用户访问网站并进行认证,服务器生成一个随机的 CSRF Token,并将其与用户关联存储在服务器端。【目前我们系统是前端根据url和时间戳自己加密生成的Token,其实不够安全】
  2. 服务器将生成的 CSRF Token 返回给前端,一般通过将其嵌入到响应的表单中,或者设置在响应的 Cookie 中。
  3. 前端页面中的表单或请求需要携带 CSRF Token。当用户提交表单或发送请求时,前端会将 CSRF Token 添加到请求参数、请求头或 Cookie 中。
  4. 服务器在接收到请求时,会验证请求中的 CSRF Token 是否与服务器端存储的 Token 匹配。如果匹配成功,则继续处理请求;否则,拒绝请求。

baseURL及不同环境分发代理

不同环境下不同baseURL

  • baseURL定义为环境变量,这样通过分发代理使在开发环境中发起axios请求时实际调用对应的测试环境/后端本地服务的接口
    • 本地开发的时候接口请求跨域了,所以需要代理分发处理下
    • 部署到测试/生产环境时如果前后端代码放同一个域名下,属于同源请求,就不需要额外处理了。如果没有放在同一域名下,也是由运维在云服务上处理反向代理,原理是一样的
  • (本地、开发和生产环境)通过不同npm命令运行项目时,跑的配置文件不同,其中apiDomaintarget也可以设置为不同:
    • 本地起服务执行的命令npm run test01package.json中对应的配置文件是.env.devTest01
      • apiDomain: /api, target: https://test01.7debao.com或者是和后端联调时的后端本地ip+端口号
    • 在jenkins打包测试和生产环境的项目代码时执行的命令是npm run static,运行的配置文件.env.production
      • apiDomain 和 target 都为空,即不设置分发代理
      • 我猜是因为项目前后端代码部署在同一域名下,接口请求不存在跨域问题,所以不需要设置反向代理,具体是运维在云服务上配的,我只能看到没有处理接口,连axios的baseUrl都没设置,看起来是同源直接请求

baseURL和proxy分发代理

  • 在Nuxt.js中,可以使用 @nuxtjs/proxy 模块来配置代理服务器,以解决跨域请求的问题
  • 二次定义axios时可规定baseURL为环境变量apiDomain(本地是/api,测试和生产是空):
    1
    2
    3
    4
    5
    6
    // 所有通过 dkAxios 实例发起的请求都会以 apiDomain 作为基础路径(本地是`/api`,测试和生产是空)
    const dkAxios = axios.create({
    withCredentials: true,
    baseURL: apiDomain,
    timeout: 60000, // 设置超时时间
    });
  • 通过buildConfig.js中的proxy代理配置来控制实际请求的转发地址:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const proxyObj = {
    target: process.env.TARGET, // 设置调用接口域名和端口号别忘了加http
    changeOrigin: true,
    pathRewrite: {
    '^/api': '', // 这里理解成用'/api'代替target里面的地址,组件中我们调接口时直接用/api代替
    // 比如我想调用'http://0.0:300/user/add',用dkAxios调用'/user/add'即可
    // 本地起服务时,经过dkAxios后为'/api/user/add',代理转发后实际调用的接口为target+'/user/add'
    // 注意:测试和生产环境的TARGET为空,即不设置分发代理,我猜是因为前后端代码被运维部署到同一域名下后不存在跨域问题,自然不需要代理分发
    },
    }
  • nuxt.config.js中给axios开启proxy代理:
    1
    2
    3
    4
    5
    axios: {
    // See https://github.com/nuxt-community/axios-module#options
    proxy: true, // 表示开启代理
    credentials: true, // 表示跨域请求时是否需要使用凭证
    },
  • 最后,开发环境中使用dkAxios请求/services/stockOthersOrder/xxx时,经过proxy代理转发后实际调用:http://test01.7debao.com/services/stockOthersOrder/xxx
    • 经过dkAxios后为/api/services/stockOthersOrder/xxx,而/api会被替换为TARGEThttps://test01.7debao.com后端本地ip+端口
  • 注意: F12 中显示的请求 URL(http://localhost:8089/api/services/stockOthersOrder/xxx) 是原始 URL(实际发送的请求 URL),并不会显示代理的转发过程,但实际上请求已经通过代理服务器转发到了 https://test01.7debao.com/services/stockOthersOrder/xxx,代理服务器会将 /api 路径重写为target里面的地址,实现请求的转发

测试环境和生产环境

  • 测试环境和生产环境配置文件中apiDomain 和 target 都为空,即不设置分发代理,不受proxy作用,我猜这是因为运维在云服务上把前端代码和后端代码部署在者同一域名下,所以不存在跨域问题,也就不需要代理分发了
  • 测试环境:
    • 部署域名http://test01.7debao.com
    • 请求接口/services/stockOthersOrder/xxx
    • 实际请求http://test01.7debao.com/services/stockOthersOrder/xxx
    • 生产环境:
      • 部署域名https://erp.7debao.com
      • 请求接口/services/stockOthersOrder/xxx
      • 实际请求https://erp.7debao.com/services/stockOthersOrder/xxx

postString postQueryParams postQuery

  • 都是自己封装的post方法,仅传参方式不同,有的情况下后端需要这几种格式,干脆封出来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 参数不从请求体传递,通过?拼接在url中传
// 注意:只接收对象类型的参数data
postString(url, data) {
let params = '?';
Object.entries(data).forEach(([k, v]) => {
params += `${k}=${v}&`;
})
params = params.slice(0, -1);
url += params;
return DkAxios({
url,
method: 'POST',
// data:{}
});
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 可接受多种类型的参数,转换为字符串(用于拼接在url后)
function paramQuery(param, key, encode) {
if (param == null) return '';
const arr = [];
const t = typeof (param);
if (t === 'string' || t === 'number' || t === 'boolean') {
arr.push(`${key}=${(encode == null || encode) ? encodeURIComponent(param) : param}`);
} else {
for (const i in param) {
const k = key == null ? i : key + (param instanceof Array ? `[${i}]` : `.${i}`);
arr.push(paramQuery(param[i], k, encode));
}
}
return arr.join('&');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 参数从url中拼接传递,不走请求体传递
postQuery(url, data = {}, type) {
let headers
const queryUrl = `${url}?${paramQuery(data)}`

if (type === 'json') {
headers = { 'Content-Type': 'application/json' }
} else {
headers = { 'Content-Type': 'application/x-www-form-urlencoded' }
}

return DkAxios({
url: queryUrl,
headers,
method: 'POST',
});
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 参数不仅通过请求体传递,还从url中拼接传递
postQueryParams(url, data = {}, type) {
let headers
const { query, params } = data
const queryUrl = `${url}?${paramQuery(query)}`
let paramsJson = params

if (type === 'json') {
headers = { 'Content-Type': 'application/json' }
} else {
headers = { 'Content-Type': 'application/x-www-form-urlencoded' }
paramsJson = qs.stringify(paramsJson)
}

return DkAxios({
url: queryUrl,
headers,
method: 'POST',
data: paramsJson,
});
},

vue2全局引入

  • 在vue2项目中,可以通过定义全局属性的方式方便后续使用封装好的axios相关方法
  • 定义全局属性:可在vue构造函数上添加该方法函数,vue实例就会通过原型链找到相应方法
    • 注意:在 Vue 的 Vue.prototype 上可以挂载对象、函数或任何类型的值,可以使其在 Vue 组件中通过 this 访问
  • 例子:
    1
    2
    3
    4
    5
    6
    7
    8
    import { FSAxios } from '@/jslibs/fs-axios';
    import fsDialog from '@/components/dialog/fs-dialog'

    // 将 对象FSAxios 作为 $FSAxios 添加到 Vue 的原型中,使其在 Vue 组件中可用,全局可以通过this.$FSAxios使用axios二次封装的FSAxios
    Vue.prototype.$FSAxios = FSAxios

    // 注册全局组件,后续可在任意位置使用<fs-dialog>
    Vue.component('fs-dialog', fsDialog)
    1
    2
    3
    4
    // 组件中可直接使用
    this.$FSAxios.get( 接口, 参数 )
    this.$FSAxios.post( 接口, 参数 )// 'application/x-www-form-urlencoded'
    this.$FSAxios.post( 接口, 参数, 'json' ) // 'application/json'
,