如何在Flutter中实现JWT token刷新?所需包有哪些?仅用HTTP包可以实现吗?
Flutter JWT Token刷新功能实现方案
核心实现流程
JWT 鉴权体系默认返回短期有效的access_token(用来发起鉴权请求)和长期有效的refresh_token(用来刷新过期的access_token),完整刷新逻辑如下:
- 首次登录成功后,将接口返回的
access_token、refresh_token、access_token过期时间三个字段持久化存储到本地 - 所有需要鉴权的业务接口,请求头中添加
Authorization: Bearer ${access_token}字段 - 接口返回401未授权状态码,或者本地判断
access_token即将过期时,触发刷新逻辑:- 加锁标记当前正在刷新token,避免多个并发请求同时触发重复刷新
- 携带本地存储的
refresh_token调用刷新接口,获取新的access_token和refresh_token - 更新本地存储的三个字段,清空请求队列里缓存的待重试请求,依次重新发起
- 如果刷新接口返回失败,说明
refresh_token也已过期,清空本地存储的用户信息,跳转登录页让用户重新登录
相关问题解答
1、实现该功能需要使用哪些依赖包?
- 核心依赖:仅需
http(网络请求)和Dart内置的dart:convert(JSON解析)即可完成基础功能 - 辅助依赖:
shared_preferences用来持久化存储token信息,避免每次APP重启都要重新登录;如果用dio网络库可以直接通过拦截器实现自动刷新逻辑,不用手动封装每个请求,开发效率更高
2、仅通过HTTP包是否可以完成该功能的开发?
可以。http包已经提供了完整的网络请求能力,你只需要手动封装通用请求方法,在方法内加入token过期判断、刷新逻辑、重试逻辑即可,不需要额外引入其他网络库。
现有代码优化示例
你当前的代码是首次登录获取token的逻辑,优化后如下,包含token存储和通用鉴权请求封装(需额外导入dart:async使用Completer):
import 'package:http/http.dart' as http; import 'dart:convert'; import 'dart:async'; import 'package:shared_preferences/shared_preferences.dart'; // 标记是否正在刷新token,避免并发重复刷新 bool _isRefreshing = false; // 请求队列,缓存刷新过程中收到的请求 List<Function> _requestQueue = []; // 首次登录获取token Future<void> getAuth(String username, String password) async { final response = await http.post(Uri.parse("你的登录接口地址"), body: { 'username': username, 'password': password, 'grant_type': "password", 'client_id': "2LJhXdj2Cu0LlVqHk2ilm1WWHtdwK**********", 'client_secret': "buQIh6tLPC0hG6QZUMm7yP7QuSjBLiffTBx3zY2HYxw95ssXQ4F85ttE5fGHInm1LBkk9GhGiHZCvoR21l4bqIOQTTJBo0nJ5******************" }); if (response.statusCode == 200) { Map<String, dynamic> responseMap = json.decode(response.body); String accessToken = responseMap['access_token']; String refreshToken = responseMap['refresh_token']; // 接口一般会返回access_token的有效时长,单位为秒 int expiresIn = responseMap['expires_in']; DateTime expireTime = DateTime.now().add(Duration(seconds: expiresIn)); // 持久化存储到本地 final prefs = await SharedPreferences.getInstance(); await prefs.setString('access_token', accessToken); await prefs.setString('refresh_token', refreshToken); await prefs.setString('expire_time', expireTime.toIso8601String()); } } // 封装通用鉴权请求,自带自动刷新token逻辑 Future<http.Response> sendAuthRequest(String url, {String method = 'GET', Map<String, dynamic>? body}) async { final prefs = await SharedPreferences.getInstance(); String? accessToken = prefs.getString('access_token'); String? refreshToken = prefs.getString('refresh_token'); String? expireTimeStr = prefs.getString('expire_time'); // 无有效登录信息,抛出异常引导跳转登录 if (accessToken == null || refreshToken == null || expireTimeStr == null) { throw Exception('未登录'); } DateTime expireTime = DateTime.parse(expireTimeStr); // 提前1分钟判断token即将过期,主动刷新避免请求失败 if (DateTime.now().isAfter(expireTime.subtract(const Duration(minutes: 1)))) { if (!_isRefreshing) { _isRefreshing = true; try { // 调用刷新token接口 final refreshResponse = await http.post(Uri.parse("你的刷新token接口地址"), body: { 'grant_type': 'refresh_token', 'refresh_token': refreshToken, 'client_id': "你的client_id", 'client_secret': "你的client_secret" }); if (refreshResponse.statusCode == 200) { Map<String, dynamic> refreshData = json.decode(refreshResponse.body); // 更新本地存储的token信息 accessToken = refreshData['access_token']; refreshToken = refreshData['refresh_token']; int newExpiresIn = refreshData['expires_in']; DateTime newExpireTime = DateTime.now().add(Duration(seconds: newExpiresIn)); await prefs.setString('access_token', accessToken!); await prefs.setString('refresh_token', refreshToken!); await prefs.setString('expire_time', newExpireTime.toIso8601String()); // 执行队列中缓存的所有请求 for (var req in _requestQueue) { req(); } _requestQueue.clear(); } else { // refresh_token也过期,清空本地数据跳转登录 await prefs.clear(); throw Exception('登录已过期,请重新登录'); } } finally { _isRefreshing = false; } } else { // 正在刷新中,把当前请求加入队列等待刷新完成后执行 Completer<http.Response> completer = Completer(); _requestQueue.add(() async { completer.complete(await sendAuthRequest(url, method: method, body: body)); }); return completer.future; } } // 构造请求头发起实际请求 Map<String, String> headers = { 'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/x-www-form-urlencoded' }; http.Response response; if (method.toUpperCase() == 'POST') { response = await http.post(Uri.parse(url), headers: headers, body: body); } else { response = await http.get(Uri.parse(url), headers: headers); } // 兜底判断,如果还是返回401,触发刷新逻辑 if (response.statusCode == 401) { return sendAuthRequest(url, method: method, body: body); } return response; }
内容的提问来源于stack exchange,提问作者Alan Bosco
相关产品推荐
相关产品推荐

