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

Flutter中基于graphql_flutter包的GraphQL拦截器实现:处理错误及401时自动刷新AccessToken

嘿,我刚好做过类似的需求,用graphql_flutter实现错误拦截和401自动刷新token其实逻辑很清晰,咱们一步步来拆解:

核心思路:用自定义Link实现拦截

在graphql_flutter里,Link是处理请求链路的核心组件,咱们可以自定义一个Link来拦截请求和响应,专门处理错误场景——尤其是401未授权的情况。

这个Link会负责给请求自动加上Authorization头,同时拦截响应,判断是否返回401状态码;如果是,就自动刷新token并重发请求。还要注意处理并发请求的情况,避免多次重复调用刷新接口:

import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class AuthLink extends Link {
  final Future<String> Function() getToken;
  final Future<String> Function() refreshToken;
  Future<String>? _ongoingRefresh; // 用来锁定刷新请求,避免并发重复刷新

  AuthLink({required this.getToken, required this.refreshToken});

  @override
  Stream<Response> request(Request request, [NextLink? forward]) async* {
    // 先获取当前存储的AccessToken
    String currentToken = await getToken();
    
    // 给请求添加Authorization头
    final authenticatedRequest = request.updateContextEntry<HttpLinkHeaders>(
      (headers) => HttpLinkHeaders(
        headers: {
          ...headers?.headers ?? {},
          'Authorization': 'Bearer $currentToken',
        },
      ),
    );

    // 发送初始请求
    final responseStream = forward!(authenticatedRequest);

    await for (final response in responseStream) {
      // 检查HTTP响应状态码是否为401
      final httpContext = response.context.entry<HttpLinkResponseContext>();
      if (httpContext?.statusCode == 401) {
        // 处理并发刷新:如果已有刷新请求在进行,直接等待它完成
        if (_ongoingRefresh == null) {
          _ongoingRefresh = refreshToken().whenComplete(() {
            // 刷新完成后清空标记
            _ongoingRefresh = null;
          });
        }

        try {
          // 获取新的AccessToken
          final newToken = await _ongoingRefresh!;
          currentToken = newToken;

          // 用新token重新构造请求并发送
          final reAuthenticatedRequest = authenticatedRequest.updateContextEntry<HttpLinkHeaders>(
            (headers) => HttpLinkHeaders(
              headers: {
                ...headers?.headers ?? {},
                'Authorization': 'Bearer $currentToken',
              },
            ),
          );

          // 返回重新请求的响应
          yield* forward(reAuthenticatedRequest);
        } catch (refreshError) {
          // 刷新失败,这里可以跳转到登录页或者抛出更明确的错误
          final updatedErrors = [
            ...?response.errors,
            GraphQLError(message: 'Token刷新失败: ${refreshError.toString()}')
          ];
          yield response.copyWith(errors: updatedErrors);
        }
      } else {
        // 非401的响应直接返回
        // 如果有其他GraphQL错误,也可以在这里统一处理(比如弹窗提示)
        if (response.errors?.isNotEmpty ?? false) {
          print('GraphQL请求错误: ${response.errors!.first.message}');
        }
        yield response;
      }
    }
  }
}
第二步:实现token的存储和刷新逻辑

你需要自己实现token的持久化存储(推荐用flutter_secure_storage存敏感信息),以及调用后端接口刷新token的逻辑:

// 示例:用flutter_secure_storage存储token
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class SecureStorage {
  final _storage = FlutterSecureStorage();
  static const _accessTokenKey = 'access_token';
  static const _refreshTokenKey = 'refresh_token';

  Future<String?> readToken() async {
    return await _storage.read(key: _accessTokenKey);
  }

  Future<String?> readRefreshToken() async {
    return await _storage.read(key: _refreshTokenKey);
  }

  Future<void> saveToken(String token) async {
    await _storage.write(key: _accessTokenKey, value: token);
  }

  Future<void> saveRefreshToken(String token) async {
    await _storage.write(key: _refreshTokenKey, value: token);
  }
}

// 刷新token的方法,替换成你自己的后端API接口
Future<String> refreshAccessToken() async {
  final refreshToken = await SecureStorage().readRefreshToken();
  if (refreshToken == null) throw Exception('没有可用的RefreshToken');

  final response = await http.post(
    Uri.parse('https://your-api-domain.com/refresh-token'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({'refreshToken': refreshToken}),
  );

  if (response.statusCode == 200) {
    final data = jsonDecode(response.body);
    final newAccessToken = data['accessToken'];
    final newRefreshToken = data['refreshToken']; // 如果后端返回新的refreshToken,记得保存
    await SecureStorage().saveToken(newAccessToken);
    await SecureStorage().saveRefreshToken(newRefreshToken);
    return newAccessToken;
  } else {
    throw Exception('刷新Token失败,HTTP状态码: ${response.statusCode}');
  }
}
第三步:整合Link到GraphQLClient

最后把自定义的AuthLink和HttpLink组合起来,创建GraphQLClient实例:

final httpLink = HttpLink('https://your-graphql-endpoint.com/graphql');

final authLink = AuthLink(
  getToken: () async => SecureStorage().readToken() ?? '',
  refreshToken: refreshAccessToken,
);

// 组合链路:先经过AuthLink处理,再发送到HttpLink
final combinedLink = authLink.concat(httpLink);

// 创建GraphQLClient实例
final graphQLClient = GraphQLClient(
  link: combinedLink,
  cache: GraphQLCache(store: HiveStore()), // 这里用Hive缓存,你也可以换成其他缓存方式
);
额外说明
  • 如果你的后端是通过GraphQL的errors字段返回401(而非HTTP状态码),那你需要在response.errors里检查对应的错误标识(比如extensions.code为UNAUTHENTICATED),再触发刷新逻辑。
  • 刷新失败后,建议直接跳转到登录页面,让用户重新登录,避免陷入无限刷新的循环。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 03:29:10