📡 Axios 拦截器:让你的每个请求都自动带上 Token
学习日志 · 2026-08-20 基于
jwt-demo/login-demo实战。前端「每次请求自动携带 Token」这件事,axios 在拦截器里默默就替你做好了。
📑 目录
- 为什么需要拦截器
- 创建统一实例:baseURL + timeout
- 请求拦截器:自动携带 Token
- 响应拦截器:直接拿 data
- 拦截器的执行顺序
- 按业务拆分的 API 模块
- 现状的坑:错误处理缺失
- 进阶:统一错误提示与 401 处理
- 踩坑记录
- 总结
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; // 只写了成功分支
}
// ❌ 没有第二个参数(失败回调)
);
这意味着:
- 网络错误、超时、HTTP 500……都会直接抛到业务代码,每个调用方都要自己 try/catch;
- 没法统一处理「401 未登录就自动跳登录页」这种全局逻辑;
- 登录页虽然 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. 总结
这次学习把「前端鉴权的公共层」彻底打通了:
- 统一实例 →
axios.create({ baseURL, timeout }),隔离、可配置; - 请求拦截器 → 自动从 localStorage 取 token,拼
Bearer头,全局生效; - 响应拦截器 → 拆出
response.data,业务层少解一层; - API 分层 → config / repo / user 各司其职,页面代码干净;
- 待补全 → 响应错误处理、401 自动跳登录(已给出生产级写法)。
一句话总结:Axios 拦截器 = 把「每次请求都带 Token」「每次响应都拆 data」这类横切逻辑收敛到一处,业务代码从此只关心数据本身。