axios 拦截器:一次封装,处处鉴权

3 阅读6分钟

前言

第 1 篇解决了"服务器怎么验证你是谁",但立刻冒出下一个问题:拿到 token 之后,怎么让每个请求都自动带上它? 登录接口在 user.js 里,业务接口在 repo.js 里,如果一个一个接口手动写 headers.Authorization,写 10 个接口就重复 10 遍,漏一个就 401。axios 的拦截器就是来解决这个的。这篇讲透三件事:axios.create() 到底创建了什么、拦截器为什么是"钩子"而不是"自己拦自己"、请求发出去和响应回来的对象各有什么。axios 的基础封装(实例化、api 目录、/api 前缀、mock 方案)已在《Axios-与-MockJS-接口请求的封装与模拟》讲过,这篇聚焦鉴权链路。

一、axios.create():不是单例,是"配置副本"

create 是 axios 的实例工厂——它基于默认配置"捏"出一个全新的独立实例:

// src/api/config.js
import axios from 'axios'

const instance = axios.create({
  baseURL: '/api',   // 统一前缀
  timeout: 5000,     // 5 秒超时
})

export default instance

它不是单例模式。 单例要求"全局唯一",而 create 想捏几个就捏几个,每个实例的 baseURLtimeout拦截器队列互不相干

const a = axios.create({ baseURL: '/a' });
const b = axios.create({ baseURL: '/b' });
a === b; // false,独立对象

// a 加拦截器,b 和默认 axios 完全不受影响
a.interceptors.request.use(...); // a 有 1 个
b.interceptors.request.handlers.length;   // 0
axios.interceptors.request.handlers.length; // 0

顺带澄清:你天天用的 axios.get()axios.post(),其实就是 axios 自带的默认实例create() 的价值在于按场景捏多套独立配置——比如"登录接口不带 token、业务接口带 token",就是两个实例各挂各的拦截器。

你项目里 config.js 只 create 了一次并 export default,靠 ES Module 模块缓存让所有 import 拿到同一个对象——这是"模块级单例效果",但是模块机制保证的,不是 axios 的设计

二、拦截器:不是"自己拦自己",是钩子

新手最容易的困惑:axios 既能发请求又能拦请求,自己发自己拦,图什么?答案是:拦截器不是 axios 拦自己,而是 axios 给你留了两个"插槽",把你的代码插进"发送前"和"接收后"

用快递公司比喻——axios 是快递公司,每次请求是一个包裹:

你的代码 →→→→→→ axios 发出包裹 →→→→→→ 服务器
              │                      │
          request 拦截器          response 拦截器
        (寄件打包台)           (收件收发室)
  • request 拦截器 = 寄件打包台:每次寄包裹都先过这里,你统一往里塞东西(token)、贴标签
  • response 拦截器 = 收件收发室:服务器回寄时先到你这,你统一拆包检查(是不是 401?要不要丢?),再交给代码

axios 只负责运送;打包台和收发室里的活,都是你安排的。这就是 AOP(面向切面编程)/ 中间件思想:不动每个请求本身,而是给"发送/接收"这两个动作加统一切面。

三、request 拦截器:自动带上 Authorization

这是鉴权的核心一环:

// src/api/config.js
instance.interceptors.request.use((config) => {
  const token = localStorage.getItem("token");
  if (token) {
    config.headers["Authorization"] = `Bearer ${token}`;
  }
  return config;   // 必须 return,否则请求发不出去!
});

三个要点:

  1. config 是请求配置对象——axios 照它组装 HTTP 报文,你在这里改它,就是改即将发出的报文
  2. 改的是 headers——Authorization: Bearer <token> 是 HTTP 标准的携带凭证方式,后端 mock/user.jsreq.headers["authorization"].split(" ")[1] 取的正是 Bearer 后的那段
  3. 必须 return config——这叫"放行"。不 return,请求就永远卡在拦截器里。这也是新手最常见的坑

有了它,业务接口一行都不用管 token:

// src/api/user.js
export const login = async (data) => {
  const res = await axios.post('/login', data);
  return res.data;
};
// src/api/repo.js
export const getRepo = async () => {
  const res = await axios.get('/repo');
  return res.data;
};

这就是"一次封装,处处鉴权":写一遍,所有请求自动带凭证。

四、response 拦截器:统一处理 401

request 拦截器用成功回调(加头),response 拦截器通常用失败回调接住 HTTP 错误——token 过期时后端返回 401,在这里统一处理,而不是每个接口各写一遍跳登录:

instance.interceptors.response.use(
  (res) => res,                              // 成功:原样放行
  (err) => {
    if (err.response?.status === 401) {      // 凭证无效/过期
      useAuthStore.getState().logout();       // 清空登录状态(zustand,见第 3 篇)
      window.location.href = '/login';        // 跳回登录页
    }
    return Promise.reject(err);               // 继续抛给调用方处理
  }
);

注意:你现在 config.js 的 response 拦截器还只是 return res 的"空转"状态。补上 401 分支,token 过期时前端才会自动清状态、跳登录——这是这个 demo 里最值得补的一环。

五、发出去的是什么:config → HTTP 报文

config 是"说明书",axios 照着它组装真实报文:

config(说明书)                         实际发出的 HTTP 报文
────────────────────────              ─────────────────────────────
method: 'post'               ──▶    POST /api/login?v=1
url: '/login'                         ↑ method + baseURL + url + params
baseURL: '/api'
params: {v:1}                       Accept: application/json
headers: {Accept, Authorization} ─▶  Authorization: Bearer xxx
data: {username, password}     ─▶    请求体: {"username":"admin","password":"123456"}

config 里核心字段就那几个:

字段作用
method请求方法
url + baseURL路径 + 前缀,拼出完整地址
paramsURL 查询参数,拼成 ?v=1
data请求体(POST 传给后端的内容,后端 req.body 就是它)
headers请求头,拦截器加 token 就是改这里
timeout超时毫秒数

最易混的两个是 paramsdataparams 拼在 URL 问号后,data 放请求体里——一个给 GET 用,一个给 POST 用。

六、回来的是什么:response 的 6 个字段

响应回来,axios 包成一个 response 对象,只有 6 个 key:

{
  data: { code: 0, user: {...}, token: '...' },  // ★ 响应体,最常用
  status: 200,           // HTTP 状态码
  statusText: 'OK',      // 状态文字
  headers: {...},        // 响应头
  config: {...},         // 这次请求用的完整配置(axios 回显)
  request: {...},        // 底层请求对象
}

你页面里读的 res.code,其实已经是剥过壳的——因为接口文件统一 return res.data

页面 await 得到 r = response      ← 完整 6 字段对象
        │
user.js: return res.data          ← 剥掉外壳,只剩 {code, user, token}
        │
Login.jsx: res.code === 0         ← 直接读业务字段

为什么要剥壳?因为你只关心服务器的业务数据code/user/token),不关心 status/headers 这些 HTTP 层细节——剥掉后页面代码更干净,这正是 axios"默默做了很多"的体现。

七、整条鉴权链路串起来

把第 1 篇和第 2 篇拼成完整闭环:

浏览器 POST /api/login (admin/123456)
   → request 拦截器(此时无 token,跳过)
   → mock/user.js: jwt.sign 签发 token
   → 前端 setAuth 存 token/user(zustand,见第 3 篇)
   → 浏览器 GET /api/repo
   → request 拦截器:自动加 Authorization: Bearer <token>
   → mock/user.js: jwt.verify 验签 → 放行返回数据
   → 若 token 过期 → 后端返回 401 → response 拦截器清状态跳登录

axios 在这里的角色是统一装卸工:把"带凭证"和"处理失效"这两个所有请求都要做的重复劳动,收拢到拦截器这一处——省事、不漏、好维护

小结

  • axios.create()实例工厂,不是单例;捏独立副本,配置/拦截器隔离;模块导出实现的"单例效果"是 ES Module 机制,不是 axios 设计
  • 拦截器是钩子不是"自己拦自己":把公共逻辑插进"发送前/接收后"两个切面,AOP 思想
  • request 拦截器自动加 Authorization: Bearer <token>,必须 return config 才算放行
  • response 拦截器用失败回调统一接 401:清状态、跳登录
  • config 是说明书,axios 照它组报文;核心字段 method/url/baseURL/params/data/headers/timeoutparams 拼 URL、data 放请求体
  • response 6 字段data/status/statusText/headers/config/request,接口封装统一 return res.data 剥壳

一句话:axios 拦截器把"带 token"和"处理 401"这两个重复劳动收拢到一处,让鉴权从"每个接口自己写"变成"一次封装,处处鉴权"。