You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Dio实现Bearer Token过期自动刷新及getToken空返回问题排查

问题处理

核心问题定位

  • getToken()返回空的核心原因:请求Token接口时未指定正确的Content-Type,绝大多数OAuth2的Token端点仅接受application/x-www-form-urlencoded格式的请求,部分接口还要求携带grant_type参数,你当前发起的无请求体、默认JSON格式的请求无法被服务端正确解析,因此返回空数据。
  • 代码附带问题:
    1. getToken()声明为Future<void>,没有返回/存储获取到的Token,业务接口只能硬编码固定Token,无法动态更新。
    2. 打印r.data时直接用+拼接字符串,如果接口返回JSON对象(Map类型)会触发类型转换异常,可能导致打印出null的假象。
    3. signInData()的请求体多套了一层大括号,实际传入的是Set类型而非Map类型,Dio序列化请求参数时会出错。
    4. 硬编码Bearer Token,过期后无法自动更新。

完整实现代码(含Token自动刷新逻辑)

实现逻辑:

  • 全局维护最新的有效Token,通过Dio拦截器统一给请求加鉴权头
  • 接口返回401状态码时自动触发Token刷新,加锁避免并发重复刷新,刷新成功后自动重试失败的请求
  • 刷新失败时清空Token,可自行扩展跳转登录页的逻辑
import 'package:dio/dio.dart';
import 'dart:convert';

// 全局配置,根据你的项目实际位置调整
late Dio _dio;
final String _baseUrl = "你的接口域名";
final String username = "你的认证用户名";
final String password = "你的认证密码";
final String VINumber = "你的VIN参数";

// 全局存储最新Token,可替换为SharedPreferences等本地持久化方案
String? _accessToken;
// Token刷新锁,防止并发请求重复触发刷新
bool _isRefreshing = false;
// 刷新期间挂起的请求队列
final List<void Function(String newToken)> _pendingRequests = [];

/// 初始化Dio,在App启动时调用
void initDio() {
  _dio = Dio(
    BaseOptions(
      baseUrl: _baseUrl,
      contentType: "application/json",
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 10),
    ),
  );

  _dio.interceptors.add(
    InterceptorsWrapper(
      // 请求拦截:统一加Token
      onRequest: (options, handler) {
        if (!options.path.contains('/services/token') && _accessToken != null) {
          options.headers["Authorization"] = "Bearer $_accessToken";
        }
        handler.next(options);
      },
      // 错误拦截:处理Token过期逻辑
      onError: (error, handler) async {
        // 401代表Token过期,且不是Token接口本身的报错
        if (error.response?.statusCode == 401 && !error.requestOptions.path.contains('/services/token')) {
          if (!_isRefreshing) {
            _isRefreshing = true;
            _pendingRequests.clear();
            // 触发Token刷新
            final newToken = await getToken();
            _isRefreshing = false;

            if (newToken != null) {
              // 刷新成功,执行所有挂起的请求
              for (final req in _pendingRequests) {
                req(newToken);
              }
              // 重试当前触发401的请求
              final reqOpts = error.requestOptions;
              final retryRes = await _dio.request(
                reqOpts.path,
                data: reqOpts.data,
                queryParameters: reqOpts.queryParameters,
                options: Options(
                  method: reqOpts.method,
                  headers: reqOpts.headers..["Authorization"] = "Bearer $newToken",
                ),
              );
              return handler.resolve(retryRes);
            } else {
              // 刷新失败,清空Token,可在此处添加跳转登录页逻辑
              _accessToken = null;
              return handler.reject(error);
            }
          } else {
            // 正在刷新Token,将当前请求加入队列
            _pendingRequests.add((token) async {
              final reqOpts = error.requestOptions;
              final retryRes = await _dio.request(
                reqOpts.path,
                data: reqOpts.data,
                queryParameters: reqOpts.queryParameters,
                options: Options(
                  method: reqOpts.method,
                  headers: reqOpts.headers..["Authorization"] = "Bearer $token",
                ),
              );
              handler.resolve(retryRes);
            });
          }
        }
        handler.next(error);
      },
    ),
  );
}

/// 修复后的获取Token方法
Future<String?> getToken() async {
  try {
    final String basicAuth = 'Basic ${base64Encode(utf8.encode('$username:$password'))}';
    debugPrint("Basic认证头: $basicAuth");

    final Response r = await _dio.post(
      '/services/token',
      // 如果你的Token接口不需要传grant_type,删掉下面的data即可
      data: {
        "grant_type": "client_credentials",
      },
      options: Options(
        headers: {"authorization": basicAuth},
        // 指定表单格式,适配绝大多数Token接口
        contentType: Headers.formUrlEncodedContentType,
        responseType: ResponseType.json,
      ),
    );

    debugPrint("Token接口状态码: ${r.statusCode}");
    debugPrint("Token接口返回: ${r.data}");

    if (r.statusCode == 200 && r.data != null) {
      // 注意:这里的access_token字段要和你实际接口返回的字段名一致,自行调整
      _accessToken = r.data['access_token'];
      return _accessToken;
    }
    return null;
  } catch (e) {
    debugPrint("获取Token失败: $e");
    return null;
  }
}

/// 修复后的业务请求方法
Future<void> signInData() async {
  try {
    // 首次请求如果没有Token,先主动获取
    _accessToken ??= await getToken();
    if (_accessToken == null) {
      debugPrint("无有效Token,无法发起业务请求");
      return;
    }

    final Response response = await _dio.post(
      '/api/gateway',
      // 去掉了多余的一层大括号,直接传Map即可
      data: {
        "ClientPackageId": "0cdd231a-d7ad-4a68-a934-d373affb5100",
        "PlatformId": "ios",
        "ClientUserId": "AhmedOmar",
        "VinNumber": VINumber
      },
    );
    debugPrint("接口返回数据: ${response.data}");
    debugPrint("请求状态码: ${response.statusCode}");
  } catch (e) {
    debugPrint("业务请求失败: $e");
  }
}

调整说明

  • 代码中r.data['access_token']的字段名需要根据你实际Token接口的返回结构调整,常见的字段名还有token、accessToken,抓包确认后修改即可。
  • 如果你的Token接口不需要传grant_type参数,直接删掉getToken里的data配置即可。
  • 如果需要持久化存储Token避免App重启后重复获取,可以把_accessToken的读写逻辑替换为SharedPreferences、Hive等本地存储方案。

内容的提问来源于stack exchange,提问作者SaDev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 14:21:07