Flutter Web中Mongo Realm基于JWT的Access Token刷新最佳实践
Flutter Web中Mongo Realm令牌刷新的最优实践
核心结论
优先采用**「请求前检查令牌有效期+401自动重试」**的方案,比固定定时刷新更高效可靠。
两种常见方案的对比
1. 固定30分钟定时刷新的问题
- 无效请求浪费:如果用户30分钟内没有操作,刷新请求完全多余
- 时间同步误差:客户端系统时间可能不准,导致提前或延迟刷新
- Web环境限制:Flutter Web中后台Worker的运行依赖页面状态,切换页面后可能暂停,无法保证定时执行
2. 请求前检查令牌的优势
- 按需刷新:只在需要发起请求时才检查,避免无意义的网络请求
- 处理边缘情况:即使检查时令牌未过期,但请求发送后刚好过期,可通过401重试机制兜底
具体实现步骤
1. 存储令牌时同步保存过期时间
获取到Mongo Realm返回的access_token后,解析其JWT payload中的exp字段(过期时间戳,单位秒),和access_token、refresh_token一起持久化(比如用shared_preferences或Web的sessionStorage)。
解析JWT的示例代码:
int parseExpiryFromToken(String token) { final parts = token.split('.'); if (parts.length != 3) throw Exception('无效的JWT令牌'); final payload = jsonDecode(utf8.decode(base64Url.decode(base64Url.normalize(parts[1])))); return payload['exp'] as int; }
2. 封装HTTP请求工具类
统一处理令牌检查、刷新和请求重试:
import 'dart:convert'; import 'dart:math'; import 'package:http/http.dart' as http; class RealmAuthClient { String? _accessToken; String? _refreshToken; int? _expiresAt; bool _isRefreshing = false; // 初始化:从本地存储读取令牌和过期时间 Future<void> init() async { // 示例:从shared_preferences读取 // final prefs = await SharedPreferences.getInstance(); // _accessToken = prefs.getString('realm_access_token'); // _refreshToken = prefs.getString('realm_refresh_token'); // _expiresAt = prefs.getInt('realm_expires_at'); } // 检查是否需要刷新(提前1分钟触发,避免请求过程中过期) bool _shouldRefresh() { if (_accessToken == null || _expiresAt == null) return true; final now = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000; return now >= _expiresAt! - 60; } // 刷新令牌 Future<void> _refreshToken() async { if (_isRefreshing || _refreshToken == null) return; _isRefreshing = true; try { final url = Uri.parse('https://realm.mongodb.com/api/client/v2.0/auth/session'); final response = await http.post( url, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $_refreshToken', }, ); if (response.statusCode == 200) { final data = jsonDecode(response.body); _accessToken = data['access_token']; _refreshToken = data['refresh_token']; _expiresAt = parseExpiryFromToken(_accessToken!); // 持久化更新后的令牌 // final prefs = await SharedPreferences.getInstance(); // prefs.setString('realm_access_token', _accessToken!); // prefs.setString('realm_refresh_token', _refreshToken!); // prefs.setInt('realm_expires_at', _expiresAt!); } else { throw Exception('刷新令牌失败: ${response.body}'); } } finally { _isRefreshing = false; } } // 统一请求方法 Future<http.Response> sendRequest( String url, { Map<String, String>? headers, dynamic body, }) async { // 先检查并刷新令牌 if (_shouldRefresh()) { await _refreshToken(); } // 构建请求头 final requestHeaders = { ...(headers ?? {}), 'Authorization': 'Bearer $_accessToken', }; // 发送请求 var response = await http.post( Uri.parse(url), headers: requestHeaders, body: body != null ? jsonEncode(body) : null, ); // 401兜底重试:处理检查后令牌刚好过期的情况 if (response.statusCode == 401) { await _refreshToken(); requestHeaders['Authorization'] = 'Bearer $_accessToken'; response = await http.post( Uri.parse(url), headers: requestHeaders, body: body != null ? jsonEncode(body) : null, ); } return response; } }
3. 额外注意事项
- 存储安全:Web环境下优先用
sessionStorage存储令牌,避免跨标签页泄露;若需持久化,注意加密处理 - 刷新失败处理:如果刷新令牌也过期(返回401),需引导用户重新登录
- 并发控制:通过
_isRefreshing锁避免多个请求同时触发刷新,减少重复请求
内容的提问来源于stack exchange,提问作者Sayyid J
相关产品推荐
相关产品推荐

