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

