HarmonyOS开发中请求拦截器链:日志、认证、重试
拦截器是网络层的"中间件",让横切逻辑优雅落地
一、为什么需要拦截器?
假设你正在开发一个需要登录的 App,每个接口都要带 token。你会怎么做?
方案A:每个请求手动加
typescript
// 页面A
http.get('/user/info', { headers: { 'Authorization': `Bearer ${token}` } });
// 页面B
http.get('/product/list', { headers: { 'Authorization': `Bearer ${token}` } });
// 页面C...(复制粘贴100遍)
等到 token 过期要刷新时,你发现要改100个地方。这时候你想砸键盘了。
方案B:拦截器自动注入
typescript
// 认证拦截器:自动给每个请求加token
class AuthInterceptor {
beforeRequest(config) {
config.headers['Authorization'] = `Bearer ${getToken()}`;
return config;
}
}
一次配置,全局生效。这就是拦截器的魅力。
拦截器能做什么?
-
请求拦截(发请求前)
- 注入认证 token
- 添加公共参数(如 app 版本、设备 ID)
- 请求日志记录
- 请求签名/加密
-
响应拦截(收到响应后)
- 统一错误处理
- token 过期自动刷新
- 响应数据转换
- 响应日志记录
-
高级能力
- 请求重试
- 缓存判断
- 请求去重
- Mock 数据注入
二、核心原理:责任链模式
拦截器链采用责任链模式:每个拦截器处理完后,传递给下一个拦截器,形成一条处理链。
flowchart LR
A[请求发起] --> B[日志拦截器<br/>记录请求信息]
B --> C[认证拦截器<br/>注入Token]
C --> D[签名拦截器<br/>参数签名]
D --> E[实际网络请求]
E --> F[错误拦截器<br/>统一处理]
F --> G[重试拦截器<br/>失败重试]
G --> H[数据转换拦截器<br/>解析响应]
H --> I[返回业务层]
J[重试请求] -.-> B
classDef primary fill:#4A90E2,stroke:#2E5C8A,color:#fff
classDef warning fill:#F5A623,stroke:#C17D10,color:#fff
classDef error fill:#E74C3C,stroke:#C0392B,color:#fff
classDef info fill:#7ED321,stroke:#5BA318,color:#fff
class A,E,I primary
class B,C,D info
class F,G error
class H warning
执行顺序:
- 请求拦截器按添加顺序正序执行
- 发起实际网络请求
- 响应拦截器按添加顺序逆序执行(像洋葱模型)
三、代码实战:五大核心拦截器
示例1:日志拦截器 - 开发调试神器
typescript
// network/interceptors/LogInterceptor.ets
import { Interceptor, RequestConfig, Response } from '../types';
/**
* 日志拦截器
* 功能:记录请求和响应的详细信息,方便调试
* 建议:仅在开发环境启用,生产环境关闭
*/
export class LogInterceptor implements Interceptor {
private enableLog: boolean;
constructor(enableLog: boolean = true) {
this.enableLog = enableLog;
}
/**
* 请求拦截:记录请求信息
*/
async beforeRequest(config: RequestConfig): Promise<RequestConfig> {
if (!this.enableLog) {
return config;
}
const timestamp = new Date().toISOString();
console.info(`\n╔════════════════════════════════════════`);
console.info(`║ [${timestamp}] 请求开始`);
console.info(`╠────────────────────────────────────────`);
console.info(`║ 方法: ${config.method?.toUpperCase() || 'GET'}`);
console.info(`║ 地址: ${config.url}`);
if (config.params && Object.keys(config.params).length > 0) {
console.info(`║ 参数: ${JSON.stringify(config.params)}`);
}
if (config.data) {
console.info(`║ 数据: ${JSON.stringify(config.data)}`);
}
if (config.headers) {
console.info(`║ 请求头: ${JSON.stringify(config.headers)}`);
}
console.info(`╚════════════════════════════════════════\n`);
// 在配置中记录请求开始时间,用于计算耗时
(config as any).__startTime = Date.now();
return config;
}
/**
* 响应拦截:记录响应信息
*/
async afterResponse<T>(response: Response<T>): Promise<Response<T>> {
if (!this.enableLog) {
return response;
}
const startTime = (response.config as any).__startTime || Date.now();
const duration = Date.now() - startTime;
const timestamp = new Date().toISOString();
console.info(`\n╔════════════════════════════════════════`);
console.info(`║ [${timestamp}] 请求完成`);
console.info(`╠────────────────────────────────────────`);
console.info(`║ 状态码: ${response.status}`);
console.info(`║ 耗时: ${duration}ms`);
console.info(`║ 地址: ${response.config.url}`);
if (response.status >= 200 && response.status < 300) {
console.info(`║ 结果: 成功`);
console.info(`║ 数据: ${JSON.stringify(response.data).substring(0, 200)}...`);
} else {
console.info(`║ 结果: 失败`);
console.info(`║ 错误: ${JSON.stringify(response.data)}`);
}
console.info(`╚════════════════════════════════════════\n`);
return response;
}
}
示例2:认证拦截器 - Token 自动注入与刷新
typescript
// network/interceptors/AuthInterceptor.ets
import { Interceptor, RequestConfig, Response, ApiError, ErrorCode } from '../types';
import { preferences } from '@kit.ArkData';
/**
* 认证拦截器
* 功能:
* 1. 自动注入 Authorization 请求头
* 2. Token 过期时自动刷新
* 3. 刷新失败时跳转登录页
*/
export class AuthInterceptor implements Interceptor {
private tokenKey: string = 'auth_token';
private refreshTokenKey: string = 'refresh_token';
private isRefreshing: boolean = false; // 防止并发刷新
private refreshSubscribers: Array<(token: string) => void> = []; // 等待刷新的请求
/**
* 请求拦截:注入Token
*/
async beforeRequest(config: RequestConfig): Promise<RequestConfig> {
// 排除不需要认证的接口
const noAuthPaths = ['/auth/login', '/auth/register', '/auth/refresh'];
const needAuth = !noAuthPaths.some(path => config.url.includes(path));
if (!needAuth) {
return config;
}
// 从本地存储获取token
const token = await this.getToken();
if (token) {
config.headers = config.headers || {};
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
}
/**
* 响应拦截:处理Token过期
*/
async afterResponse<T>(response: Response<T>): Promise<Response<T>> {
// 检查是否Token过期(通常后端返回401)
if (response.status === 401) {
// 尝试刷新Token
const newToken = await this.handleTokenExpired();
if (newToken) {
// Token刷新成功,重试原请求
return this.retryRequest<T>(response.config, newToken);
} else {
// Token刷新失败,跳转登录页
this.redirectToLogin();
throw new ApiError('登录已过期,请重新登录', ErrorCode.UNAUTHORIZED, 401);
}
}
return response;
}
/**
* 获取存储的Token
* @private
*/
private async getToken(): Promise<string | null> {
try {
// 从Preferences获取token
const dataPreferences = await preferences.getPreferences('app_storage');
return await dataPreferences.get(this.tokenKey, '') as string;
} catch (error) {
console.error('[AuthInterceptor] 获取Token失败:', error);
return null;
}
}
/**
* 处理Token过期
* @private
*/
private async handleTokenExpired(): Promise<string | null> {
// 如果已经在刷新中,等待刷新完成
if (this.isRefreshing) {
return new Promise((resolve) => {
this.refreshSubscribers.push(resolve);
});
}
this.isRefreshing = true;
try {
// 获取refreshToken
const dataPreferences = await preferences.getPreferences('app_storage');
const refreshToken = await dataPreferences.get(this.refreshTokenKey, '') as string;
if (!refreshToken) {
return null;
}
// 调用刷新Token接口(这里需要单独处理,避免循环拦截)
const newToken = await this.callRefreshTokenApi(refreshToken);
// 保存新Token
await dataPreferences.put(this.tokenKey, newToken);
await dataPreferences.flush();
// 通知所有等待的请求
this.refreshSubscribers.forEach(callback => callback(newToken));
this.refreshSubscribers = [];
return newToken;
} catch (error) {
console.error('[AuthInterceptor] Token刷新失败:', error);
return null;
} finally {
this.isRefreshing = false;
}
}
/**
* 调用刷新Token接口
* @private
*/
private async callRefreshTokenApi(refreshToken: string): Promise<string> {
// 这里直接使用原生http,避免循环拦截
// 实际项目中可以创建一个不经过拦截器的http实例
const http = await import('@ohos.net.http');
const httpRequest = http.createHttp();
try {
const response = await httpRequest.request('https://api.myapp.com/v1/auth/refresh', {
method: http.RequestMethod.POST,
header: { 'Content-Type': 'application/json' },
extraData: { refreshToken }
});
const result = JSON.parse(response.result as string);
return result.data.token;
} finally {
httpRequest.destroy();
}
}
/**
* 重试原请求
* @private
*/
private async retryRequest<T>(config: RequestConfig, newToken: string): Promise<Response<T>> {
// 更新请求头中的Token
config.headers = config.headers || {};
config.headers['Authorization'] = `Bearer ${newToken}`;
// 这里需要重新发起请求
// 实际实现中需要注入HttpClient实例
// 简化示例,抛出特殊错误让外层重试
throw new ApiError('Token已刷新,请重试', -100, 0);
}
/**
* 跳转到登录页
* @private
*/
private redirectToLogin(): void {
// 使用路由跳转到登录页
// 这里需要根据实际路由方案实现
console.warn('[AuthInterceptor] Token过期,需要跳转登录页');
// router.pushUrl({ url: 'pages/LoginPage' });
}
}
示例3:重试拦截器 - 网络抖动自动恢复
typescript
// network/interceptors/RetryInterceptor.ets
import { Interceptor, RequestConfig, Response, ApiError } from '../types';
/**
* 重试拦截器
* 功能:请求失败时自动重试,应对网络不稳定场景
* 配置:最大重试次数、重试延迟、可重试的错误码
*/
export class RetryInterceptor implements Interceptor {
private maxRetries: number; // 最大重试次数
private retryDelay: number; // 重试延迟(毫秒)
private retryableStatuses: number[]; // 可重试的HTTP状态码
constructor(config?: {
maxRetries?: number;
retryDelay?: number;
retryableStatuses?: number[];
}) {
this.maxRetries = config?.maxRetries ?? 3;
this.retryDelay = config?.retryDelay ?? 1000;
this.retryableStatuses = config?.retryableStatuses ?? [408, 429, 500, 502, 503, 504];
}
/**
* 响应拦截:失败时重试
*/
async afterResponse<T>(response: Response<T>): Promise<Response<T>> {
// 请求成功,直接返回
if (response.status >= 200 && response.status < 300) {
return response;
}
// 检查是否可重试
if (!this.retryableStatuses.includes(response.status)) {
return response;
}
// 获取已重试次数
const retryCount = (response.config as any).__retryCount || 0;
// 达到最大重试次数,放弃
if (retryCount >= this.maxRetries) {
console.warn(`[RetryInterceptor] 已达最大重试次数(${this.maxRetries}),放弃重试`);
return response;
}
// 记录重试次数
(response.config as any).__retryCount = retryCount + 1;
console.info(`[RetryInterceptor] 第${retryCount + 1}次重试,延迟${this.retryDelay}ms`);
// 延迟后重试
await this.delay(this.retryDelay * (retryCount + 1)); // 指数退避
// 抛出特殊错误,触发外层重试
throw new ApiError(
`请求失败,正在重试(${retryCount + 1}/${this.maxRetries})`,
-200,
response.status
);
}
/**
* 延迟函数
* @private
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
示例4:签名拦截器 - 接口安全加固
typescript
// network/interceptors/SignatureInterceptor.ets
import { Interceptor, RequestConfig } from '../types';
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
/**
* 签名拦截器
* 功能:对请求参数进行签名,防止接口被恶意调用
* 原理:将参数按规则排序后拼接,用密钥进行HMAC-SHA256签名
*/
export class SignatureInterceptor implements Interceptor {
private secretKey: string;
private appId: string;
constructor(config: { secretKey: string; appId: string }) {
this.secretKey = config.secretKey;
this.appId = config.appId;
}
/**
* 请求拦截:添加签名
*/
async beforeRequest(config: RequestConfig): Promise<RequestConfig> {
// 只对需要签名的接口处理(通常是POST/PUT/DELETE)
const needSignMethods = ['POST', 'PUT', 'DELETE'];
if (!needSignMethods.includes(config.method?.toUpperCase() || '')) {
return config;
}
// 准备签名数据
const timestamp = Date.now().toString();
const nonce = this.generateNonce();
// 合并所有参数
const allParams = {
...config.params,
...config.data,
_appId: this.appId,
_timestamp: timestamp,
_nonce: nonce
};
// 生成签名
const sign = await this.generateSign(allParams);
// 添加签名相关参数
config.headers = config.headers || {};
config.headers['X-App-Id'] = this.appId;
config.headers['X-Timestamp'] = timestamp;
config.headers['X-Nonce'] = nonce;
config.headers['X-Signature'] = sign;
return config;
}
/**
* 生成签名
* @private
*/
private async generateSign(params: Record<string, any>): Promise<string> {
// 1. 按key字典序排序
const sortedKeys = Object.keys(params).sort();
// 2. 拼接成 key=value 格式
const paramString = sortedKeys
.map(key => `${key}=${params[key]}`)
.join('&');
// 3. HMAC-SHA256 签名
try {
const mac = cryptoFramework.createMac('SHA256');
const keyBlob: cryptoFramework.DataBlob = {
data: new Uint8Array(Buffer.from(this.secretKey, 'utf-8').buffer)
};
const symKey = await cryptoFramework.createSymKey(keyBlob);
await mac.init(symKey);
const dataBlob: cryptoFramework.DataBlob = {
data: new Uint8Array(Buffer.from(paramString, 'utf-8').buffer)
};
const result = await mac.update(dataBlob);
// 转十六进制字符串
return Array.from(result.data)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
} catch (error) {
console.error('[SignatureInterceptor] 签名生成失败:', error);
throw error;
}
}
/**
* 生成随机字符串
* @private
*/
private generateNonce(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let nonce = '';
for (let i = 0; i < 16; i++) {
nonce += chars.charAt(Math.floor(Math.random() * chars.length));
}
return nonce;
}
}
示例5:错误处理拦截器 - 统一异常处理
typescript
// network/interceptors/ErrorInterceptor.ets
import { Interceptor, Response, ApiError, ErrorCode } from '../types';
import { promptAction } from '@kit.ArkUI';
/**
* 错误处理拦截器
* 功能:统一处理各类错误,提供友好的用户提示
*/
export class ErrorInterceptor implements Interceptor {
private showErrorToast: boolean; // 是否显示错误提示
constructor(showErrorToast: boolean = true) {
this.showErrorToast = showErrorToast;
}
/**
* 响应拦截:统一错误处理
*/
async afterResponse<T>(response: Response<T>): Promise<Response<T>> {
// 请求成功,直接返回
if (response.status >= 200 && response.status < 300) {
return response;
}
// 解析错误信息
const error = this.parseError(response);
// 显示错误提示
if (this.showErrorToast) {
this.showErrorMessage(error.message);
}
// 抛出统一错误
throw error;
}
/**
* 解析错误信息
* @private
*/
private parseError(response: Response<any>): ApiError {
const status = response.status;
const data = response.data;
// 尝试从响应中提取错误消息
let message = '请求失败';
let code = ErrorCode.SERVER_ERROR;
if (data && typeof data === 'object') {
message = data.message || data.msg || data.error || message;
code = data.code || code;
}
// 根据HTTP状态码补充错误信息
switch (status) {
case 400:
message = message || '请求参数错误';
code = ErrorCode.BAD_REQUEST;
break;
case 401:
message = '未登录或登录已过期';
code = ErrorCode.UNAUTHORIZED;
break;
case 403:
message = '没有访问权限';
code = ErrorCode.FORBIDDEN;
break;
case 404:
message = '请求的资源不存在';
code = ErrorCode.NOT_FOUND;
break;
case 408:
message = '请求超时';
code = ErrorCode.TIMEOUT;
break;
case 500:
message = '服务器内部错误';
code = ErrorCode.SERVER_ERROR;
break;
case 502:
message = '网关错误';
break;
case 503:
message = '服务暂时不可用';
break;
case 504:
message = '网关超时';
break;
}
return new ApiError(message, code, status, data);
}
/**
* 显示错误提示
* @private
*/
private showErrorMessage(message: string): void {
try {
promptAction.showToast({
message: message,
duration: 2000
});
} catch (error) {
console.error('[ErrorInterceptor] 显示Toast失败:', error);
}
}
}
四、踩坑与注意事项
坑1:拦截器执行顺序很重要
问题:签名拦截器在认证拦截器之前执行,导致签名计算不包含 token。
解决:按依赖关系排序:
typescript
// ✅ 正确顺序
httpClient.use(new LogInterceptor()); // 1. 日志(最外层)
httpClient.use(new SignatureInterceptor()); // 2. 签名(需要完整参数)
httpClient.use(new AuthInterceptor()); // 3. 认证(最后注入token)
httpClient.use(new ErrorInterceptor()); // 4. 错误处理(响应拦截时最先执行)
坑2:Token 刷新的并发问题
问题:多个请求同时 401,触发多次 token 刷新。
解决:使用标志位 + 等待队列(见 AuthInterceptor 实现)。
坑3:重试拦截器的死循环
问题:重试后还是失败,又触发重试,无限循环。
解决:限制最大重试次数,使用指数退避:
typescript
// 指数退避:每次重试延迟时间递增
const delay = this.retryDelay * Math.pow(2, retryCount);
坑4:拦截器中的异步操作
问题:拦截器返回 Promise,但调用方没 await。
解决:确保拦截器调用都是 async/await:
typescript
// ❌ 错误:忘记await
for (const interceptor of this.interceptors) {
interceptor.beforeRequest(config); // 没等待!
}
// ✅ 正确
for (const interceptor of this.interceptors) {
config = await interceptor.beforeRequest(config);
}
五、HarmonyOS 6 适配要点
1. 加密 API 变更
typescript
// HarmonyOS 5.x
import cryptoFramework from '@ohos.security.cryptoFramework';
// HarmonyOS 6
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
2. Preferences API 增强
typescript
// HarmonyOS 6 支持异步初始化
const dataPreferences = await preferences.getPreferences('app_storage', {
name: 'my_app_storage' // 可以指定存储名称
});
3. Toast 提示优化
typescript
// HarmonyOS 6 支持更多配置
promptAction.showToast({
message: '操作成功',
duration: 2000,
bottom: 100, // 距离底部距离
showMode: promptAction.ToastShowMode.TOP_MOVED // 显示模式
});
六、总结
拦截器是网络层的"瑞士军刀",掌握它就掌握了请求的全生命周期:
| 拦截器 | 请求拦截 | 响应拦截 | 核心能力 |
|---|---|---|---|
| LogInterceptor | ✅ 记录请求信息 | ✅ 记录响应信息 | 调试利器 |
| AuthInterceptor | ✅ 注入Token | ✅ 刷新Token | 认证保障 |
| RetryInterceptor | - | ✅ 失败重试 | 容错能力 |
| SignatureInterceptor | ✅ 参数签名 | - | 安全加固 |
| ErrorInterceptor | - | ✅ 统一处理 | 用户体验 |
记住几个原则:
- ✅ 单一职责:一个拦截器只做一件事
- ✅ 顺序敏感:按依赖关系排序
- ✅ 异步安全:所有操作都返回 Promise
- ✅ 可配置化:关键参数支持外部配置
下一篇我们深入响应解析器,看看如何优雅处理 JSON、XML、Protobuf 等多种数据格式。
💡 提示:生产环境记得关闭 LogInterceptor,或者使用环境变量控制,避免敏感信息泄露。