Flutter中如何实现JWT Token过期时自动无感刷新
Flutter 静默刷新JWT Token无感知实现方案
要实现用户无感知的后台Token自动刷新,核心是把所有鉴权、刷新逻辑收敛到网络请求统一拦截层,完全不侵入业务代码,用户操作全程不会被打断,具体实现方案如下:
核心设计原则
- 业务层不需要感知Token过期、刷新逻辑,正常调用接口即可
- 并发请求同时触发Token过期时,仅发起1次刷新请求,避免重复调用刷新接口把RefreshToken刷作废
- 刷新过程中所有在途请求自动挂起,拿到新Token后批量重试,用户操作、页面跳转完全不受影响
- 仅当RefreshToken也失效、无法完成续期时,才触发登录跳转兜底逻辑
具体实现(基于Dio网络库)
Dio是Flutter生态最常用的网络库,自带拦截器机制可以低成本实现整套逻辑:
- 前置准备
- 用
flutter_secure_storage做本地持久化,加密存储accessToken、refreshToken,不要用普通明文存储存敏感凭证 - 给全局Dio实例配置请求、错误拦截器,统一处理请求头Token注入、过期错误拦截
- 用
- 拦截器核心代码
import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class AuthInterceptor extends Interceptor { final Dio dio; final FlutterSecureStorage _storage = const FlutterSecureStorage(); // 刷新锁,避免并发重复发起刷新请求 bool _isRefreshing = false; // 挂起队列:存刷新期间触发的待重试请求 final List<Future<void> Function()> _pendingRequests = []; AuthInterceptor(this.dio); @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { // 跳过标记为不需要鉴权的请求(比如刷新Token接口、登录接口) if (options.extra['skipAuth'] == true) { return handler.next(options); } // 统一注入请求头Token final accessToken = await _storage.read(key: 'accessToken'); if (accessToken != null) { options.headers['Authorization'] = 'Bearer $accessToken'; } handler.next(options); } @override void onError(DioError err, ErrorInterceptorHandler handler) async { // 和后端对齐Token过期的判断规则,一般是401状态码,部分后端用自定义业务码 final isTokenExpired = err.response?.statusCode == 401; if (!isTokenExpired) return handler.next(err); // 不在刷新状态,触发刷新流程 if (!_isRefreshing) { _isRefreshing = true; try { final refreshToken = await _storage.read(key: 'refreshToken'); // 刷新接口标记skipAuth,避免死循环 final refreshRes = await dio.post('/auth/refresh', data: {'refreshToken': refreshToken}, options: Options(extra: {'skipAuth': true})); // 存储新的Token对 final newAccessToken = refreshRes.data['accessToken']; final newRefreshToken = refreshRes.data['refreshToken']; await _storage.write(key: 'accessToken', value: newAccessToken); await _storage.write(key: 'refreshToken', value: newRefreshToken); // 批量重试所有挂起的请求 for (final pendingReq in _pendingRequests) { pendingReq(); } _pendingRequests.clear(); // 重试当前触发401的请求,返回结果给业务层 final retryRes = await _retryRequest(err.requestOptions); return handler.resolve(retryRes); } catch (e) { // 刷新失败:一般是RefreshToken过期、账号被顶登,清空本地凭证触发登录跳转 _pendingRequests.clear(); await _storage.deleteAll(); // 用全局事件总线发过期事件,在根路由层监听处理跳转,不要在拦截器里直接拿context跳页 // eventBus.fire(TokenInvalidEvent()); return handler.next(err); } finally { _isRefreshing = false; } } // 正在刷新中,把当前请求加入挂起队列 final completer = Completer<Response>(); _pendingRequests.add(() async { final res = await _retryRequest(err.requestOptions); completer.complete(res); }); handler.resolve(await completer.future); } // 用新Token重试原请求 Future<Response> _retryRequest(RequestOptions originalReq) async { final newToken = await _storage.read(key: 'accessToken'); return dio.request( originalReq.path, data: originalReq.data, queryParameters: originalReq.queryParameters, options: Options( method: originalReq.method, headers: {...originalReq.headers, 'Authorization': 'Bearer $newToken'}, extra: originalReq.extra, ), ); } }
避坑说明
- 不要做「Token快过期提前刷新」的逻辑,本地设备时间和服务端时间很容易出现偏差,提前刷新的稳定性远不如接口返回过期后再触发刷新
- 刷新全程不要弹全局Loading、Toast提示,保证流程完全静默,用户感知不到后台的刷新操作
- 大部分后端的RefreshToken是一次性的,并发发多个刷新请求会直接把Token作废,加锁逻辑不能省
- 如果用其他网络库,逻辑完全通用:统一请求入口、加锁防并发刷新、挂起请求重试、失效兜底跳登录即可
内容的提问来源于stack exchange,提问作者Rozan Shrestha
相关产品推荐
相关产品推荐

