Dio与OAuth2集成:实现令牌管理与刷新机制

【免费下载链接】dio A powerful HTTP client for Dart and Flutter, which supports global settings, Interceptors, FormData, aborting and canceling a request, files uploading and downloading, requests timeout, custom adapters, etc. 【免费下载链接】dio 项目地址: https://gitcode.com/gh_mirrors/di/dio

OAuth2(开放授权2.0)是现代应用中常用的身份验证协议,而Dio作为Dart和Flutter生态中强大的HTTP客户端,通过拦截器(Interceptor)机制可以优雅地实现令牌的自动管理与刷新。本文将详细介绍如何基于Dio的拦截器架构,构建完整的OAuth2令牌生命周期管理方案,解决令牌过期、并发请求冲突等实际问题。

拦截器架构:Dio的扩展核心

Dio的拦截器系统基于责任链模式设计,允许开发者在请求发送前、响应返回后或发生错误时介入处理流程。核心拦截器接口定义在dio/lib/src/interceptor.dart中,主要包含三个生命周期方法:

  • onRequest: 请求发送前触发,可修改请求参数(如添加认证头)
  • onResponse: 响应返回后触发,可处理响应数据
  • onError: 请求出错时触发,可捕获401等状态码并执行刷新逻辑
class OAuth2Interceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // 添加Token到请求头
    options.headers['Authorization'] = 'Bearer ${_tokenManager.accessToken}';
    handler.next(options); // 传递给下一个拦截器
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401) {
      // 处理令牌过期逻辑
      final bool refreshed = await _refreshToken();
      if (refreshed) {
        // 刷新成功后重试原请求
        handler.resolve(await _retryRequest(err.requestOptions));
        return;
      }
    }
    handler.next(err); // 传递错误给下一个拦截器
  }
}

拦截器执行流程

Dio的拦截器队列采用FIFO(先进先出)顺序执行,通过handler.next()方法将控制权传递给下一个拦截器。当需要中断流程时,可使用handler.resolve()直接返回响应或handler.reject()抛出错误。这种设计使得令牌刷新逻辑可以无缝集成到现有请求流程中。

令牌管理器:核心状态管理

为实现令牌的存储、验证和刷新功能,我们需要构建一个令牌管理器(TokenManager)。该管理器应至少包含以下功能:

  • 安全存储访问令牌(Access Token)和刷新令牌(Refresh Token)
  • 检查令牌有效期
  • 执行令牌刷新请求
  • 处理并发刷新场景

基础令牌管理器实现

class TokenManager {
  String? _accessToken;
  String? _refreshToken;
  DateTime? _tokenExpiry;

  // 从持久化存储初始化令牌
  Future<void> init() async {
    _accessToken = await _secureStorage.read(key: 'access_token');
    _refreshToken = await _secureStorage.read(key: 'refresh_token');
    _tokenExpiry = DateTime.tryParse(await _secureStorage.read(key: 'expiry') ?? '');
  }

  // 检查令牌是否有效(提前60秒过期以应对网络延迟)
  bool get isTokenValid => _accessToken != null && 
      _tokenExpiry != null && 
      _tokenExpiry!.isAfter(DateTime.now().add(const Duration(seconds: 60)));

  // 刷新令牌实现
  Future<bool> refresh() async {
    try {
      final response = await dio.post(
        'https://auth.example.com/token',
        data: {
          'grant_type': 'refresh_token',
          'refresh_token': _refreshToken,
          'client_id': 'your_client_id'
        },
      );
      
      _accessToken = response.data['access_token'];
      _refreshToken = response.data['refresh_token'];
      _tokenExpiry = DateTime.now().add(
        Duration(seconds: response.data['expires_in']),
      );
      
      // 持久化存储新令牌
      await _persistTokens();
      return true;
    } catch (e) {
      await clearTokens(); // 刷新失败时清除令牌
      return false;
    }
  }
}

线程安全与并发控制

在多线程环境(如Flutter应用)中,并发请求可能导致多个刷新令牌请求同时发起。为解决此问题,可使用互斥锁(Mutex)确保同一时间只有一个刷新流程执行:

final _refreshLock = Lock(); // 使用synchronized包

Future<bool> refresh() async {
  return _refreshLock.synchronized(() async {
    // 双重检查:获取锁后再次验证令牌是否已刷新
    if (isTokenValid) return true;
    
    // 执行实际刷新逻辑...
  });
}

完整集成方案:拦截器+令牌管理器

将令牌管理器与Dio拦截器结合,形成完整的OAuth2认证流程。以下是集成关键点:

1. 初始化Dio实例与拦截器链

final dio = Dio();
final tokenManager = TokenManager();
await tokenManager.init();

// 添加OAuth2拦截器
dio.interceptors.add(OAuth2Interceptor(tokenManager));
// 添加日志拦截器以便调试
dio.interceptors.add(LogInterceptor(responseBody: true));

2. 请求拦截器:添加认证头

onRequest阶段,拦截器检查令牌有效性并添加Authorization头:

@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
  if (!tokenManager.isTokenValid) {
    // 令牌已过期且无刷新令牌,直接终止请求
    if (tokenManager.refreshToken == null) {
      handler.reject(DioException(
        requestOptions: options,
        type: DioExceptionType.cancel,
        error: '认证令牌不存在',
      ));
      return;
    }
    // 主动触发令牌刷新(适用于预检测场景)
    _refreshTokenAndProceed(options, handler);
    return;
  }
  
  // 添加Bearer令牌
  options.headers['Authorization'] = 'Bearer ${tokenManager.accessToken}';
  handler.next(options);
}

3. 错误拦截器:处理401响应

当服务器返回401(未授权)状态码时,在onError阶段执行令牌刷新并重试请求:

@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
  if (err.response?.statusCode == 401) {
    // 尝试刷新令牌
    final bool refreshed = await tokenManager.refresh();
    if (refreshed) {
      // 刷新成功,使用新令牌重试原请求
      final response = await _retryOriginalRequest(err.requestOptions);
      handler.resolve(response);
      return;
    } else {
      // 刷新失败,触发重新登录
      _authBloc.add(AuthLogoutEvent());
    }
  }
  handler.next(err);
}

// 重试原请求方法
Future<Response<dynamic>> _retryOriginalRequest(RequestOptions options) async {
  final newOptions = options.copyWith();
  newOptions.headers['Authorization'] = 'Bearer ${tokenManager.accessToken}';
  return dio.request<dynamic>(
    newOptions.path,
    method: newOptions.method,
    data: newOptions.data,
    queryParameters: newOptions.queryParameters,
    options: newOptions,
  );
}

4. 避免无限刷新循环

为防止因刷新令牌无效导致的无限循环,需在请求中添加标记以区分普通请求和刷新请求:

// 刷新令牌请求标记
const _refreshTokenMarker = 'is_refresh_request';

// 刷新令牌时标记请求
Future<bool> refresh() async {
  final options = Options(extra: {_refreshTokenMarker: true});
  final response = await dio.post(
    '/token',
    data: {'grant_type': 'refresh_token'},
    options: options,
  );
  // ...
}

// 在拦截器中跳过刷新请求的认证检查
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
  if (options.extra.containsKey(_refreshTokenMarker) && options.extra[_refreshTokenMarker] == true) {
    handler.next(options); // 刷新请求直接放行
    return;
  }
  // ...正常令牌检查逻辑
}

高级优化:提升用户体验与系统稳定性

1. 请求队列管理

当令牌刷新过程中存在多个并发请求时,可通过请求队列将后续请求挂起,待刷新完成后统一重试。实现方式如下:

class RequestQueue {
  final List<Completer<Response>> _queue = [];
  bool _isRefreshing = false;

  // 添加请求到队列
  Future<Response> enqueue(Future<Response> Function() request) async {
    if (!_isRefreshing) {
      return request();
    }
    
    final completer = Completer<Response>();
    _queue.add(completer);
    return completer.future;
  }

  // 刷新完成后重试所有队列请求
  void retryAll() {
    _isRefreshing = false;
    for (final completer in _queue) {
      completer.complete(request()); // 重试原请求
    }
    _queue.clear();
  }
}

2. 持久化存储方案

对于令牌的持久化存储,推荐使用Flutter Secure Storage或Hive加密存储,避免明文存储敏感信息:

// 使用flutter_secure_storage
final _secureStorage = const FlutterSecureStorage();

Future<void> _persistTokens() async {
  await _secureStorage.write(key: 'access_token', value: _accessToken);
  await _secureStorage.write(key: 'refresh_token', value: _refreshToken);
  await _secureStorage.write(key: 'expiry', value: _tokenExpiry?.toIso8601String());
}

3. 证书固定(Certificate Pinning)

为增强安全性,可启用Dio的证书固定功能,防止中间人攻击。Dio提供了证书固定的适配器实现:

// 配置SSL证书固定
dio.httpClientAdapter = DefaultHttpClientAdapter()
  ..onHttpClientCreate = (client) {
    client.badCertificateCallback = (cert, host, port) => false;
    // 添加可信证书
    SecurityContext.defaultContext.setTrustedCertificates('assets/cert.pem');
    return client;
  };

完整代码示例与项目结构

推荐的项目结构如下,将认证相关代码模块化组织:

lib/
├── api/
│   ├── dio_client.dart      # Dio实例配置
│   └── interceptors/
│       ├── oauth2_interceptor.dart  # OAuth2拦截器
│       └── log_interceptor.dart     # 日志拦截器
├── auth/
│   ├── token_manager.dart   # 令牌管理逻辑
│   └── auth_bloc.dart       # 认证状态管理
└── utils/
    └── secure_storage.dart  # 安全存储工具

初始化Dio客户端示例

// dio_client.dart
class DioClient {
  final Dio _dio;
  final TokenManager _tokenManager;

  DioClient(this._tokenManager) : _dio = Dio() {
    _configureDio();
  }

  Dio get instance => _dio;

  void _configureDio() {
    _dio.options.baseUrl = 'https://api.example.com';
    _dio.options.connectTimeout = const Duration(seconds: 5);
    _dio.options.receiveTimeout = const Duration(seconds: 3);
    
    // 添加拦截器
    _dio.interceptors.add(OAuth2Interceptor(_tokenManager));
    _dio.interceptors.add(LogInterceptor(
      requestBody: true,
      responseBody: true,
    ));
  }
}

总结与最佳实践

Dio与OAuth2的集成核心在于利用拦截器机制实现令牌的自动管理。通过本文介绍的方案,开发者可以构建安全、可靠的认证系统,主要关键点包括:

  1. 职责分离:将令牌管理逻辑封装在TokenManager中,拦截器专注于请求流程控制
  2. 并发安全:使用锁机制和请求队列避免并发刷新冲突
  3. 异常处理:完善的错误恢复机制确保应用稳定性
  4. 安全存储:采用加密方式存储敏感令牌信息
  5. 状态管理:结合Bloc或Provider实现认证状态的全局管理

通过这种架构,应用可以实现无感的令牌刷新体验,同时保证在各种异常场景下的稳定性和安全性。建议在实际项目中根据具体需求扩展令牌管理器功能,如添加令牌撤销、多账户支持等高级特性。

【免费下载链接】dio A powerful HTTP client for Dart and Flutter, which supports global settings, Interceptors, FormData, aborting and canceling a request, files uploading and downloading, requests timeout, custom adapters, etc. 【免费下载链接】dio 项目地址: https://gitcode.com/gh_mirrors/di/dio

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐