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

Flutter登录认证:如何实现刷新Token及全局传递API Key与Token

解决方案

一、完善登录函数,持久化存储凭证

你的登录接口返回应包含api_key、access_token、refresh_token以及expires_in(Token有效期,单位秒)。我们用shared_preferences做本地持久化,确保APP重启后仍能保留凭证。

先在pubspec.yaml添加依赖:

dependencies:
  shared_preferences: ^2.2.2 # 可替换为最新版本

修改后的登录函数:

import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

Future<bool> login(String username, String password) async {
  final client = http.Client();
  try {
    final response = await client.post(
      Uri.parse('https://flutter.noviindus.co.in/api/LoginApi'),
      body: {'username': username, 'password': password},
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      // 按实际返回字段调整key名
      final apiKey = data['api_key'];
      final accessToken = data['access'];
      final refreshToken = data['refresh'];
      final expiresIn = data['expires_in'];

      // 计算Token过期时间戳(当前时间+有效期毫秒数)
      final expiryTime = DateTime.now().millisecondsSinceEpoch + (expiresIn * 1000);

      // 写入本地存储
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('api_key', apiKey);
      await prefs.setString('access_token', accessToken);
      await prefs.setString('refresh_token', refreshToken);
      await prefs.setInt('token_expiry', expiryTime);

      return true;
    } else {
      print('登录失败:${response.body}');
      return false;
    }
  } catch (e) {
    print('请求异常:$e');
    return false;
  } finally {
    client.close();
  }
}

二、封装HTTP客户端,自动处理Token刷新与Header注入

手动给每个请求加Header太繁琐,还容易遗漏Token过期处理。封装一个通用客户端,自动完成凭证校验、Token刷新、Header注入:

import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

class ApiClient {
  final http.Client _client = http.Client();
  static const baseUrl = 'https://flutter.noviindus.co.in/api';

  // 获取本地存储的登录凭证
  Future<Map<String, dynamic>?> _getCredentials() async {
    final prefs = await SharedPreferences.getInstance();
    final apiKey = prefs.getString('api_key');
    final accessToken = prefs.getString('access_token');
    final refreshToken = prefs.getString('refresh_token');
    final expiryTime = prefs.getInt('token_expiry');

    if (apiKey == null || accessToken == null || refreshToken == null || expiryTime == null) {
      return null;
    }

    return {
      'api_key': apiKey,
      'access_token': accessToken,
      'refresh_token': refreshToken,
      'is_expired': DateTime.now().millisecondsSinceEpoch > expiryTime,
    };
  }

  // 调用刷新Token接口
  Future<bool> _refreshToken() async {
    final prefs = await SharedPreferences.getInstance();
    final refreshToken = prefs.getString('refresh_token');
    if (refreshToken == null) return false;

    try {
      final response = await _client.post(
        Uri.parse('$baseUrl/RefreshTokenApi'), // 替换为你的刷新接口地址
        body: {'refresh': refreshToken},
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        final newAccessToken = data['access'];
        final newExpiresIn = data['expires_in'];
        final newExpiryTime = DateTime.now().millisecondsSinceEpoch + (newExpiresIn * 1000);

        await prefs.setString('access_token', newAccessToken);
        await prefs.setInt('token_expiry', newExpiryTime);
        return true;
      } else {
        // 刷新失败,清除本地凭证,引导重新登录
        await prefs.clear();
        return false;
      }
    } catch (e) {
      print('刷新Token异常:$e');
      return false;
    }
  }

  // 通用请求方法,支持GET/POST/PUT/DELETE
  Future<http.Response> request(
    String method,
    String path, {
    Map<String, dynamic>? body,
    Map<String, String>? headers,
  }) async {
    var credentials = await _getCredentials();
    if (credentials == null) {
      throw Exception('未登录,请先登录');
    }

    // Token过期则先刷新
    if (credentials['is_expired']) {
      final refreshSuccess = await _refreshToken();
      if (!refreshSuccess) {
        throw Exception('Token过期且刷新失败,请重新登录');
      }
      // 刷新后重新获取凭证
      credentials = await _getCredentials();
      if (credentials == null) {
        throw Exception('登录状态失效,请重新登录');
      }
    }

    // 构建通用请求头
    final defaultHeaders = {
      'X-API-Key': credentials['api_key'], // 按后端要求调整Header字段名
      'Authorization': 'Bearer ${credentials['access_token']}',
      'Content-Type': 'application/json',
      ...?headers, // 合并自定义Header
    };

    final url = Uri.parse('$baseUrl/$path');
    http.Response response;

    switch (method.toUpperCase()) {
      case 'GET':
        response = await _client.get(url, headers: defaultHeaders);
        break;
      case 'POST':
        response = await _client.post(
          url,
          headers: defaultHeaders,
          body: body != null ? jsonEncode(body) : null,
        );
        break;
      case 'PUT':
        response = await _client.put(
          url,
          headers: defaultHeaders,
          body: body != null ? jsonEncode(body) : null,
        );
        break;
      case 'DELETE':
        response = await _client.delete(url, headers: defaultHeaders);
        break;
      default:
        throw Exception('不支持的请求方法');
    }

    // 若返回401,尝试再次刷新Token并重试请求
    if (response.statusCode == 401) {
      final refreshSuccess = await _refreshToken();
      if (refreshSuccess) {
        credentials = await _getCredentials();
        if (credentials != null) {
          final retryHeaders = {
            'X-API-Key': credentials['api_key'],
            'Authorization': 'Bearer ${credentials['access_token']}',
            'Content-Type': 'application/json',
            ...?headers,
          };
          // 重试原请求
          switch (method.toUpperCase()) {
            case 'GET':
              response = await _client.get(url, headers: retryHeaders);
              break;
            case 'POST':
              response = await _client.post(
                url,
                headers: retryHeaders,
                body: body != null ? jsonEncode(body) : null,
              );
              break;
            case 'PUT':
              response = await _client.put(
                url,
                headers: retryHeaders,
                body: body != null ? jsonEncode(body) : null,
              );
              break;
            case 'DELETE':
              response = await _client.delete(url, headers: retryHeaders);
              break;
          }
        }
      }
    }

    return response;
  }
}

三、使用封装后的客户端发起请求

之后所有API请求直接调用ApiClient,无需手动处理Header和Token:

// 示例:获取用户信息
Future<void> getUserInfo() async {
  final apiClient = ApiClient();
  try {
    final response = await apiClient.request('GET', 'UserInfo');
    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      print('用户信息:$data');
    } else {
      print('获取用户信息失败:${response.body}');
    }
  } catch (e) {
    print('请求异常:$e');
    // 此处可跳转至登录页面
  }
}

关于Header传递的补充说明

你之前在Header里传Token的思路是对的:

  • access_token通常放在Authorization头,格式为Bearer <token>
  • API Key一般放在自定义Header(如X-API-Key),具体字段名需和后端确认

额外注意点:

  • 建议给刷新Token逻辑加锁(比如用mutex包),避免并发请求重复刷新
  • Token过期时间要准确计算,提前判断减少无效请求
  • 刷新Token失败时,务必清除本地凭证,避免残留无效数据

内容的提问来源于stack exchange,提问作者Midhun Ap

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 00:45:36