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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 19:52:32