📡 Axios 拦截器:让你的每个请求都自动带上 Token

17 阅读4分钟

📡 Axios 拦截器:让你的每个请求都自动带上 Token

学习日志 · 2026-08-20 基于 jwt-demo/login-demo 实战。前端「每次请求自动携带 Token」这件事,axios 在拦截器里默默就替你做好了。


📑 目录

  1. 为什么需要拦截器
  2. 创建统一实例:baseURL + timeout
  3. 请求拦截器:自动携带 Token
  4. 响应拦截器:直接拿 data
  5. 拦截器的执行顺序
  6. 按业务拆分的 API 模块
  7. 现状的坑:错误处理缺失
  8. 进阶:统一错误提示与 401 处理
  9. 踩坑记录
  10. 总结

1. 为什么需要拦截器

回想一下,如果没有拦截器,前端想带 Token 得这样写:

// 每个接口都要手动带 token,重复代码爆炸
const res1 = await axios.get('/api/repo', {
    headers: { authorization: `Bearer ${localStorage.getItem('token')}` }
});
const res2 = await axios.post('/api/pay', data, {
    headers: { authorization: `Bearer ${localStorage.getItem('token')}` }
});

问题:

  • 每个接口复制粘贴一遍 header 拼装;
  • 后端返回格式变了({code, data}),每个地方都要解一层;
  • 换个 header 名(authorization → Authorization),全局改。

拦截器 = 给「每一次请求 / 每一次响应」统一装一个过滤器,公共逻辑写一遍,全局生效。

              ┌────────────────────────────┐
 请求发出前    │  请求拦截器(request)       │
   config ──► │  自动塞 Bearer Token        │ ──► 发往服务器
              └────────────────────────────┘

              ┌────────────────────────────┐
 响应回来后    │  响应拦截器(response)      │
   原始响应 ─► │  拆出 response.data         │ ──► 交给业务代码
              └────────────────────────────┘

2. 创建统一实例:baseURL + timeout

src/api/config.js:

import axios from 'axios';

// 创建一个专属实例,不污染全局 axios 配置
const instance = axios.create({
    baseURL: '/api',      // 所有请求自动拼上前缀:/api/repo
    timeout: 10000,       // 10 秒超时
});

2.1 为什么要 axios.create()

方式影响
直接改 axios.defaults.baseURL全局生效,所有用了 axios 的代码都被改
axios.create({...})只对这个实例生效,隔离干净

项目里 api 模块统一 import instance from './config',互不干扰。

2.2 baseURL 的妙用

  • 业务代码写 /repo、/login 这种相对路径;
  • 实例自动拼成 /api/repo、/api/login;
  • 后期要换网关、加版本号,只改一处。

💡 配上 Vite 的 vite-plugin-mock,/api 前缀正好和 mock 接口的 URL 对齐(见 Mock 篇)。


3. 请求拦截器:自动携带 Token

核心逻辑,就这一段:

instance.interceptors.request.use(
    (config) => {
        // 从 localStorage 取出登录时存的 token
        const token = localStorage.getItem('token');
        if (token) {
            // 拼成标准的 Bearer 认证头
            config.headers['authorization'] = `Bearer ${token}`;
        }
        // 请求配置对象原样返回,交给下一个环节
        return config;
    },
    (error) => {
        return Promise.reject(error);
    }
);

3.1 逐行拆解

代码作用
interceptors.request.use(成功回调, 失败回调)注册请求拦截器
config请求配置对象(url、method、headers、params、data...)
config.headers['authorization'] = ...给请求头塞认证信息
if (token)没 token 就不加头,避免带个空 Bearer
return config必须返回,不返回请求就断了
Promise.reject(error)请求配置阶段出错时透传错误

3.2 效果

登录成功 → token 存进 localStorage
      │
      ▼
我调用 getRepo()
      │
      ▼
请求拦截器自动读出 token
      │
      ▼
config.headers.authorization = "Bearer eyJhbGci..."
      │
      ▼
请求带着 Token 发往 /api/repo

从此业务代码只需要写 axios.get('/repo'),Token 的事全权交给拦截器——这就是「axios 默默地做了很多」的含义。


4. 响应拦截器:直接拿 data

instance.interceptors.response.use(
    (response) => {
        // 直接返回 response.data,业务代码少解一层
        return response.data;
    }
);

4.1 没有它的时候

// ❌ 每个接口都要写 .data
const res = await axios.get('/repo');
console.log(res.data.code, res.data.data);

4.2 有它之后

// ✅ 拦截器已把 response.data 拆出来
const res = await axios.get('/repo');
console.log(res.code, res.data);   // 直接就是业务数据结构

登录页的代码就很舒服:

const res = await login(formData);
if (res.code === 0) {          // 不用再写 res.data.code
    setAuth({ token: res.token, user: res.user });
}

💡 一个前提:mock 后端返回 { code, data/user/token } 这种「code 包裹」格式,拦截器拆 response.data 后,业务层拿到的正好是这个对象。前后端约定好统一响应格式,是拦截器方案的前提。


5. 拦截器的执行顺序

多拦截器的顺序容易记反,这里画清楚:

请求:请求拦截器【后注册的先执行,先注册的后执行】
响应:响应拦截器【先注册的先执行】

        ┌──────────────┐      ┌──────────────┐
 请求   │ 拦截器A       │      │ 拦截器B       │     服务器
 ────► │ (后注册先跑)  │ ───► │ (先注册后跑)  │ ─────────►
        └──────────────┘      └──────────────┘

        ┌──────────────┐      ┌──────────────┐
 响应   │ 拦截器A       │      │ 拦截器B       │     服务器
 ◄──── │ (先注册先跑)  │ ◄─── │ (后注册后跑)  │ ◄─────────
        └──────────────┘      └──────────────┘

对本项目:只注册了一个请求拦截器和一个响应拦截器,按「请求先、响应后」理解即可。真正复杂系统里顺序问题才会凸显。


6. 按业务拆分的 API 模块

src/api/ 目录结构:

src/api/
  ├── config.js    ← axios 实例 + 拦截器(所有请求的公共层)
  ├── repo.js      ← 仓储相关接口
  └── user.js      ← 用户相关接口
// repo.js
import axios from './config';
export const getRepo = async () => {
    const res = await axios.get('/repo');
    return res;
};

// user.js
import axios from './config';
export const login = async (data) => {
    const res = await axios.post('/login', data);
    return res;
};

分层思路:

业务组件(Login.jsx)
     │  只关心业务函数
     ▼
API 模块(user.js / repo.js)     ← 只写 url + 方法,不含鉴权细节
     │  复用同一个带拦截器的实例
     ▼
config.js(axios 实例 + 拦截器)   ← 统一 baseURL / Token / 拆 data

📌 好处:页面代码看不到 axios、看不到 localStorage、看不到 header 拼装,换接口、改鉴权方式,只动 api 层。


7. 现状的坑:错误处理缺失

看项目现状,响应拦截器只处理了成功回调:

instance.interceptors.response.use(
    (response) => {
        return response.data;   // 只写了成功分支
    }
    // ❌ 没有第二个参数(失败回调)
);

这意味着:

  1. 网络错误、超时、HTTP 500……都会直接抛到业务代码,每个调用方都要自己 try/catch;
  2. 没法统一处理「401 未登录就自动跳登录页」这种全局逻辑;
  3. 登录页虽然 try/catch 了,但其他页面就得各自为战。

这是本项目的「待优化点」,第 8 节给出补全方案。


8. 进阶:统一错误提示与 401 处理

补全后的响应拦截器(生产推荐):

instance.interceptors.response.use(
    // ✅ 成功:直接返回 data
    (response) => response.data,

    // ✅ 失败:统一处理
    (error) => {
        // HTTP 层错误(401/403/500/超时…)
        if (error.response) {
            const { status } = error.response;
            if (status === 401) {
                // Token 失效 → 清掉登录态,跳去登录页
                localStorage.removeItem('token');
                localStorage.removeItem('user');
                window.location.href = '/login';
            } else {
                // 其他状态码:统一弹错误提示
                console.error(`请求失败:${error.message}`);
            }
        } else if (error.request) {
            // 请求发出去了但没收到响应(超时/断网)
            console.error('网络异常,请检查网络连接');
        } else {
            // 配置阶段就错了
            console.error('请求配置错误:', error.message);
        }
        return Promise.reject(error);   // 继续抛,让调用方决定
    }
);

进阶后,**「Token 失效 → 自动踢回登录页」**这个高频需求,就不用在每个页面重复写了。


9. 踩坑记录

⚠️ 踩坑 1:拦截器回调忘了 return

请求拦截器必须 return config,响应拦截器必须 return response.data(或 response)。不返回,请求/响应就「断在半路」。

⚠️ 踩坑 2:config.headers 可能为 undefined

部分场景下 headers 为空对象。更稳妥的写法:

config.headers = config.headers || {};
config.headers['authorization'] = `Bearer ${token}`;

⚠️ 踩坑 3:Bearer 拼写/大小写不一致

后端取头时 auth.split(' ')[1],依赖的是 Bearer (空格分隔)。前端如果拼成 Bearer${token}(没空格)或 Bearer 大小写不一致,后端就取不到正确 token。

⚠️ 踩坑 4:token 别塞进 cookie

JWT 放 localStorage 或内存都行,但别放 cookie——Cookie 会自动随每次请求发送,加大 CSRF 风险。本项目存 localStorage,由拦截器手动带,是对的。

⚠️ 踩坑 5:响应拦截器拆了 data,别重复拆

业务代码里再写一次 res.data.code 会拿不到(已经是拆过的)。约定好「拦截器拆层,业务层直接消费业务字段」。


10. 总结

这次学习把「前端鉴权的公共层」彻底打通了:

  1. 统一实例 → axios.create({ baseURL, timeout }),隔离、可配置;
  2. 请求拦截器 → 自动从 localStorage 取 token,拼 Bearer 头,全局生效;
  3. 响应拦截器 → 拆出 response.data,业务层少解一层;
  4. API 分层 → config / repo / user 各司其职,页面代码干净;
  5. 待补全 → 响应错误处理、401 自动跳登录(已给出生产级写法)。

一句话总结:Axios 拦截器 = 把「每次请求都带 Token」「每次响应都拆 data」这类横切逻辑收敛到一处,业务代码从此只关心数据本身。