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

refresh token刷新后重试API请求 修复无效access token报错

问题产生原因
  • 重试逻辑未被等待,返回值错误。401分支中用Future.delayed包裹的重试请求没有加await,属于脱离外层函数生命周期的“悬空异步任务”,外层函数会直接返回第一次请求拿到的401响应,根本不会把重试后的结果返回给调用方。
  • 重试请求的认证请求头字段错误。首次请求使用标准的Authorization字段传递Bearer Token,重试时错误换成了x-access-token字段,后端无法从标准位置读取到新Token,自然判定访问令牌无效。
  • 无并发刷新保护。如果同时有多个接口触发401,会重复调用Token刷新接口,部分后端实现中刷新Token仅可使用一次,重复调用会导致刚拿到的新Refresh Token失效,后续所有请求全部认证失败。
  • 异常分支缺失。Token刷新接口调用失败(比如Refresh Token本身过期)时没有做处理,会继续带着无效Token发起重试,必然返回认证错误。
可行修复方案

核心修复思路是修正异步逻辑写法、统一请求头、增加并发锁、补全异常处理,修复后的完整代码如下:

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
// 省略你原有ApiConstants等业务导入

class ApiService {
  String? accessToken;
  String? refreshToken;
  bool status = false;
  final http.Client _ioClient = IOClient();
  // 新增并发锁标记,防止重复刷新Token
  bool _isRefreshing = false;

  Future<http.Response> _getAPIById(String pathURL) async {
    try {
      final Uri uri = Uri.http(ApiConstants.baseUrl, pathURL);
      
      // 统一构造认证请求头,避免字段写错
      Map<String, String> _buildAuthHeaders() {
        return {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer $accessToken'
        };
      }

      // 首次发起请求
      http.Response response = await _ioClient.get(
        uri,
        headers: _buildAuthHeaders(),
      );

      // 触发Token刷新逻辑:仅当返回401时执行
      if (response.statusCode == 401) {
        // 无正在进行的刷新任务时,才发起刷新请求
        if (!_isRefreshing) {
          _isRefreshing = true;
          try {
            await refreshAccessToken();
          } catch (_) {
            _isRefreshing = false;
            rethrow;
          }
          _isRefreshing = false;
        } else {
          // 已有刷新任务在执行,等待刷新完成
          while (_isRefreshing) {
            await Future.delayed(const Duration(milliseconds: 50));
          }
        }

        // 刷新完成后用新Token重试原请求,直接覆盖原响应对象
        response = await _ioClient.get(
          uri,
          headers: _buildAuthHeaders(),
        );
      }

      // 统一处理响应日志和返回
      if (response.statusCode == 200) {
        print("请求成功");
        print(response.body);
      } else {
        print("请求异常,状态码:${response.statusCode}");
        print(response.body);
      }
      return response;
    } catch (e) {
      print("请求出错:$e");
      throw Exception('接口请求失败');
    }
  }

  Future<String> refreshAccessToken() async {
    if (refreshToken == null) {
      throw Exception('登录凭证已失效');
    }
    Map<String, String> requestBody = {'refresh_token': refreshToken!};
    final response = await http.post(
      Uri.parse(ApiConstants.baseUrlWithHttp + ApiConstants.refreshAccessToken),
      body: requestBody,
      headers: {
        "Accept": "application/json",
        "Content-Type": "application/x-www-form-urlencoded",
      },
      encoding: Encoding.getByName("utf-8"),
    );

    print("执行Token刷新");
    print(response.body);
    if (response.statusCode == 200) {
      final decodedData = jsonDecode(response.body);
      accessToken = decodedData['accessToken'];
      // 后端返回新Refresh Token时才更新,避免覆盖有效值
      if (decodedData['refreshToken'] != null) {
        refreshToken = decodedData['refreshToken'];
      }
      status = true;
      return response.body;
    } else {
      // 刷新失败,清空本地登录状态,供上层跳转登录页
      accessToken = null;
      refreshToken = null;
      status = false;
      throw Exception('登录状态已过期,请重新登录');
    }
  }
}

核心改动说明

  • 移除了无等待的Future.delayed逻辑,Token刷新完成后立刻发起重试,重试结果直接覆盖原响应对象统一返回,不会再返回第一次的401错误响应。
  • 抽离统一的请求头构造方法,所有请求固定使用标准Authorization字段传递Token,修复了之前重试时请求头字段写错的问题。
  • 新增_isRefreshing并发锁,多接口同时触发401时仅执行一次刷新操作,其余请求等待刷新完成后再重试,避免重复刷新导致Token失效。
  • 补全全链路异常处理:Refresh Token无效、刷新接口调用失败时,会清空本地登录态并抛出明确错误,方便上层做重新登录的跳转逻辑,不会继续发起无效重试。
  • 简化冗余的状态码判断逻辑,首次请求和重试请求的响应统一处理,避免分支漏洞。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 19:18:26