HarmonyOS开发中请求拦截器链:日志、认证、重试

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;
  }
}

一次配置,全局生效。这就是拦截器的魅力。

拦截器能做什么?

  1. 请求拦截(发请求前)

    • 注入认证 token
    • 添加公共参数(如 app 版本、设备 ID)
    • 请求日志记录
    • 请求签名/加密
  2. 响应拦截(收到响应后)

    • 统一错误处理
    • token 过期自动刷新
    • 响应数据转换
    • 响应日志记录
  3. 高级能力

    • 请求重试
    • 缓存判断
    • 请求去重
    • 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. 请求拦截器按添加顺序正序执行
  2. 发起实际网络请求
  3. 响应拦截器按添加顺序逆序执行(像洋葱模型)

三、代码实战:五大核心拦截器

示例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,或者使用环境变量控制,避免敏感信息泄露。

相关推荐
Rust研习社21 小时前
组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?
后端·rust·编程语言
Jack201 天前
HarmonyOS开发中RESTful API封装:网络层架构设计
编程语言
用户497863050732 天前
(一)小红的数组操作
算法·编程语言
Rust研习社2 天前
这 8 个 Rust 学习资源值得每个新手收藏起来
后端·rust·编程语言
Moonbit2 天前
MoonBit ×CCF开源创新大赛 倒计时24天!快来提交你的作品
程序员·编程语言
Rust研习社3 天前
Rust 错误处理的黄金搭档:一个定义错误,一个传播错误
后端·rust·编程语言
2601_951643724 天前
1 章 C语言概述
c语言·编程语言·历史·标准·优缺点
大熊猫侯佩5 天前
SwiftData 迁移深度指南:从入门到“填坑”(下集)
数据库·swift·编程语言
大熊猫侯佩5 天前
SwiftData 迁移深度指南:从入门到“填坑”(上集)
数据库·swift·编程语言