Flutter 登录状态管理与 Token 持久化方案

Posted on 2025-11-18 13:06  lachesism  阅读(9)  评论(0)    收藏  举报

概述

该方案实现了 Flutter 应用的登录状态管理和 Token 持久化,包含:
  • 登录信息本地持久化(SharedPreferences)
  • 应用启动时自动恢复登录状态
  • 网络请求自动添加 Token
  • 登录状态全局管理(Provider)
  • 登录状态变化事件通知

依赖配置

在 pubspec.yaml 中添加以下依赖:

dependencies:
  flutter:
    sdk: flutter
  # 状态管理
  provider: ^6.0.0
  # 本地存储
  flustars: ^1.0.0
  # 网络请求
  dio: ^5.0.0
  # 事件总线(可选,用于登录状态变化通知)
  event_bus: ^2.0.0
  # JSON 序列化(可选,如果使用 json_serializable)
  json_annotation: ^4.8.0

dev_dependencies:
  # JSON 序列化代码生成(可选)
  json_serializable: ^6.7.0
  build_runner: ^2.4.0

 

完整代码实现

1. 常量定义文件 lib/common/constant.dart

/// 应用常量定义
class Constant {
  /// 登录信息缓存键名
  /// 用于在 SharedPreferences 中存储登录信息的键
  static const String cacheLoginInfo = 'loginInfo';
  
  // 可以根据需要添加其他常量
  // static const String cacheUserId = 'userId';
  // static const String cacheToken = 'token';
}

 2. 登录数据模型 lib/models/login_bean.dart

import 'package:json_annotation/json_annotation.dart';

/// 登录信息数据模型
/// 包含服务器返回的登录相关信息
/// 
/// 如果使用 json_serializable,需要运行:
/// flutter pub run build_runner build
@JsonSerializable()
class LoginBean {
  /// 认证令牌,用于后续 API 请求的身份验证
  final String token;

  /// 用户ID
  final int userId;

  /// 用户编号(可根据实际业务调整字段)
  final int userNo;

  LoginBean({
    required this.token,
    required this.userId,
    required this.userNo,
  });

  /// 从 JSON 创建对象
  factory LoginBean.fromJson(Map<String, dynamic> json) =>
      _$LoginBeanFromJson(json);

  /// 转换为 JSON
  Map<String, dynamic> toJson() => _$LoginBeanToJson(this);
}

// 如果使用 json_serializable,取消下面的注释并运行 build_runner
// part 'login_bean.g.dart';

 如果不使用 json_serializable,可以使用手动序列化:

/// 登录信息数据模型(手动序列化版本)
class LoginBean {
  final String token;
  final int userId;
  final int userNo;

  LoginBean({
    required this.token,
    required this.userId,
    required this.userNo,
  });

  /// 从 JSON 创建对象
  factory LoginBean.fromJson(Map<String, dynamic> json) {
    return LoginBean(
      token: json['token'] as String,
      userId: json['userId'] as int,
      userNo: json['userNo'] as int,
    );
  }

  /// 转换为 JSON
  Map<String, dynamic> toJson() {
    return {
      'token': token,
      'userId': userId,
      'userNo': userNo,
    };
  }
}

 3. 用户信息工具类 lib/utils/user_utils.dart

import 'dart:convert';
import 'package:flustars/flustars.dart';
import 'package:your_app/common/constant.dart';
import 'package:your_app/models/login_bean.dart';

/// 用户信息管理工具类
/// 
/// 职责:
/// 1. 保存登录信息到本地存储(SharedPreferences)
/// 2. 从本地存储读取登录信息
/// 3. 清除登录信息
/// 
/// 使用单例模式,确保全局只有一个实例
class UserUtils {
  // 单例模式实现
  static UserUtils? _instance;
  
  /// 工厂构造函数,返回单例实例
  factory UserUtils() => _getInstance();
  
  /// 静态 getter,方便直接访问实例
  static UserUtils get instance => _getInstance();
  
  /// 获取单例实例
  static UserUtils _getInstance() {
    _instance ??= UserUtils._internal();
    return _instance!;
  }

  /// 私有构造函数,防止外部直接创建实例
  UserUtils._internal();

  /// 从本地存储获取登录信息
  /// 
  /// 返回值:
  /// - 如果存在登录信息,返回 LoginBean 对象
  /// - 如果不存在或解析失败,返回 null
  /// 
  /// 使用场景:
  /// - 应用启动时恢复登录状态
  /// - 检查用户是否已登录
  LoginBean? getLoginInfo() {
    try {
      // 从 SharedPreferences 读取存储的 JSON 字符串
      String? loginInfoString = SpUtil.getString(
        Constant.cacheLoginInfo,
        defValue: null,
      );
      
      // 如果存在数据,解析为 LoginBean 对象
      if (loginInfoString != null && loginInfoString.isNotEmpty) {
        // 将 JSON 字符串解析为 Map,然后创建 LoginBean 对象
        Map<String, dynamic> jsonMap = json.decode(loginInfoString);
        LoginBean loginInfo = LoginBean.fromJson(jsonMap);
        return loginInfo;
      }
    } catch (e) {
      // 捕获异常,避免因解析失败导致应用崩溃
      // 实际项目中可以使用日志框架记录错误
      print("获取缓存中的登录信息失败:$e");
    }
    return null;
  }

  /// 保存登录信息到本地存储
  /// 
  /// 参数:
  /// - loginInfo: 要保存的登录信息,如果为 null 则清除已保存的信息
  /// 
  /// 使用场景:
  /// - 登录成功后保存用户信息
  /// - 退出登录时清除用户信息(传入 null)
  void save(LoginBean? loginInfo) {
    try {
      if (loginInfo == null) {
        // 如果传入 null,清除已保存的登录信息
        SpUtil.remove(Constant.cacheLoginInfo);
      } else {
        // 将 LoginBean 对象转换为 JSON 字符串并保存
        String jsonString = json.encode(loginInfo.toJson());
        SpUtil.putString(Constant.cacheLoginInfo, jsonString);
      }
    } catch (e) {
      // 捕获异常,避免因保存失败导致应用崩溃
      print("缓存登录信息失败:$e");
    }
  }

  /// 清除登录信息(可选方法,与 save(null) 功能相同)
  void clearLoginInfo() {
    save(null);
  }
}

 4. 登录状态管理 Provider lib/providers/login_provider.dart

import 'package:flutter/material.dart';
import 'package:your_app/models/login_bean.dart';
import 'package:your_app/utils/user_utils.dart';
// 如果使用事件总线,取消下面的注释
// import 'package:event_bus/event_bus.dart';
// import 'package:your_app/events/login_event.dart';

/// 登录状态管理 Provider
/// 
/// 职责:
/// 1. 管理全局登录状态
/// 2. 在应用启动时自动恢复登录状态
/// 3. 提供登录/登出方法
/// 4. 通知其他组件登录状态变化
/// 
/// 使用 ChangeNotifier 实现状态管理,配合 Provider 使用
class LoginProvider extends ChangeNotifier {
  // 单例模式实现
  static LoginProvider? _instance;
  
  /// 工厂构造函数,返回单例实例
  factory LoginProvider() => _getInstance();
  
  /// 获取单例实例
  static LoginProvider _getInstance() {
    _instance ??= LoginProvider._internal();
    return _instance!;
  }

  /// 当前登录信息
  /// 如果为 null,表示未登录
  LoginBean? loginInfo;

  /// 私有构造函数
  /// 在构造函数中自动从本地存储恢复登录状态
  LoginProvider._internal() {
    // 应用启动时,从本地存储读取登录信息
    loginInfo = UserUtils.instance.getLoginInfo();
    
    // 打印日志,方便调试(实际项目中可以使用日志框架)
    if (loginInfo != null) {
      print("已恢复登录状态,token: ${loginInfo?.token}");
    } else {
      print("未找到登录信息,用户未登录");
    }
  }

  /// 判断用户是否已登录
  /// 
  /// 返回值:
  /// - true: 已登录
  /// - false: 未登录
  bool get isLogin => loginInfo != null;

  /// 更新登录信息
  /// 
  /// 参数:
  /// - loginInfo: 新的登录信息,如果为 null 则表示退出登录
  /// 
  /// 功能:
  /// 1. 更新内存中的登录信息
  /// 2. 持久化保存到本地存储
  /// 3. 发送登录状态变化事件(如果使用事件总线)
  /// 4. 通知所有监听者状态已更新
  /// 
  /// 使用场景:
  /// - 登录成功后调用,传入服务器返回的登录信息
  /// - 退出登录时调用,传入 null
  void updateLoginInfo(LoginBean? loginInfo) {
    // 更新内存中的登录信息
    this.loginInfo = loginInfo;
    
    // 持久化保存到本地存储
    UserUtils.instance.save(loginInfo);

    // 如果使用事件总线,发送登录状态变化事件
    // 其他组件可以监听这个事件,响应登录状态变化
    // eventBus.fire(LoginEvent(isLogin: isLogin));

    // 通知所有监听者状态已更新
    // 使用 Provider 的组件会自动重建
    notifyListeners();
  }

  /// 退出登录
  /// 
  /// 功能:
  /// 1. 清除登录信息
  /// 2. 跳转到登录页面(可选)
  /// 
  /// 使用场景:
  /// - 用户主动退出登录
  /// - Token 过期需要重新登录
  void logout() {
    // 清除登录信息(传入 null)
    updateLoginInfo(null);
    
    // 如果需要跳转到登录页面,可以在这里添加导航逻辑
    // Navigator.of(context).pushNamedAndRemoveUntil(
    //   '/login',
    //   (route) => false,
    // );
  }
}

 5. 事件定义(可选)lib/events/login_event.dart

/// 登录状态变化事件
/// 
/// 当用户登录或退出登录时,会发送此事件
/// 其他组件可以监听此事件,响应登录状态变化
/// 
/// 使用场景:
/// - 购物车需要根据登录状态显示不同内容
/// - 个人中心页面需要根据登录状态显示不同信息
/// - 需要实时响应登录状态变化的场景
class LoginEvent {
  /// 是否已登录
  final bool isLogin;

  LoginEvent({required this.isLogin});
}

 6. 事件总线(可选)lib/events/event_bus.dart

import 'package:event_bus/event_bus.dart';

/// 全局事件总线实例
/// 
/// 用于在应用的不同部分之间传递事件
/// 例如:登录状态变化、用户信息更新等
/// 
/// 使用方式:
/// 1. 发送事件:eventBus.fire(LoginEvent(isLogin: true));
/// 2. 监听事件:eventBus.on<LoginEvent>().listen((event) { ... });
/// 3. 取消监听:subscription.cancel();
final EventBus eventBus = EventBus();

 7. Token 拦截器 lib/net/interceptors/token_interceptor.dart

import 'package:dio/dio.dart';
import 'package:your_app/providers/login_provider.dart';

/// Token 拦截器
/// 
/// 功能:
/// 在每次网络请求时,自动在请求头中添加 Token
/// 
/// 工作原理:
/// 1. 拦截所有发出的 HTTP 请求
/// 2. 检查用户是否已登录
/// 3. 如果已登录,在请求头中添加 Access_token
/// 
/// 使用方式:
/// 在创建 Dio 实例时,添加到拦截器列表中:
/// dio.interceptors.add(TokenInterceptor());
class TokenInterceptor extends Interceptor {
  /// 请求拦截器
  /// 在请求发送前执行
  @override
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) {
    // 获取登录状态管理实例
    LoginProvider provider = LoginProvider();
    
    // 如果用户已登录,在请求头中添加 Token
    if (provider.isLogin && provider.loginInfo != null) {
      // 根据后端要求设置 Token 字段名
      // 常见的有:Authorization, Access_token, Token, X-Auth-Token 等
      options.headers['Access_token'] = provider.loginInfo!.token;
      
      // 如果需要使用 Bearer Token 格式:
      // options.headers['Authorization'] = 'Bearer ${provider.loginInfo!.token}';
    }

    // 继续处理请求
    super.onRequest(options, handler);
  }

  /// 响应拦截器(可选)
  /// 在收到响应后执行
  /// 可以在这里处理 Token 过期等情况
  @override
  void onResponse(
    Response response,
    ResponseInterceptorHandler handler,
  ) {
    // 可以在这里检查响应状态码
    // 例如:如果返回 401(未授权),说明 Token 过期,可以自动退出登录
    // if (response.statusCode == 401) {
    //   LoginProvider().logout();
    // }
    
    super.onResponse(response, handler);
  }

  /// 错误拦截器(可选)
  /// 在请求出错时执行
  @override
  void onError(
    DioException err,
    ErrorInterceptorHandler handler,
  ) {
    // 可以在这里处理 Token 相关的错误
    // 例如:401 错误时自动退出登录
    // if (err.response?.statusCode == 401) {
    //   LoginProvider().logout();
    // }
    
    super.onError(err, handler);
  }
}

 8. Dio 配置示例 lib/net/dio_config.dart

import 'package:dio/dio.dart';
import 'package:your_app/net/interceptors/token_interceptor.dart';

/// Dio 网络请求配置
/// 
/// 创建配置好的 Dio 实例,包含 Token 拦截器
class DioConfig {
  static Dio? _dio;

  /// 获取配置好的 Dio 实例
  static Dio getInstance() {
    _dio ??= Dio(
      BaseOptions(
        // 基础 URL(根据实际情况修改)
        baseUrl: 'https://api.example.com',
        // 连接超时时间(毫秒)
        connectTimeout: const Duration(seconds: 30),
        // 接收超时时间(毫秒)
        receiveTimeout: const Duration(seconds: 30),
        // 请求头
        headers: {
          'Content-Type': 'application/json',
        },
      ),
    );

    // 添加 Token 拦截器
    // 这样所有通过此 Dio 实例发送的请求都会自动添加 Token
    _dio!.interceptors.add(TokenInterceptor());

    return _dio!;
  }
}

 9. 在应用入口配置 Provider lib/main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app/providers/login_provider.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      // 注册全局 Provider
      providers: [
        // 登录状态管理 Provider
        // 使用 ChangeNotifierProvider.value 确保使用单例实例
        ChangeNotifierProvider.value(
          value: LoginProvider(),
        ),
        // 可以在这里添加其他 Provider
      ],
      child: MaterialApp(
        title: 'Your App',
        // ... 其他配置
      ),
    );
  }
}

 10. 登录页面使用示例 lib/pages/login/login_page.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app/models/login_bean.dart';
import 'package:your_app/providers/login_provider.dart';
import 'package:your_app/net/dio_config.dart';
import 'package:dio/dio.dart';

class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final TextEditingController _phoneController = TextEditingController();
  final TextEditingController _codeController = TextEditingController();

  /// 登录按钮点击事件
  Future<void> _handleLogin() async {
    try {
      // 1. 发送登录请求
      Dio dio = DioConfig.getInstance();
      Response response = await dio.post(
        '/api/login',
        data: {
          'phone': _phoneController.text,
          'code': _codeController.text,
        },
      );

      // 2. 解析响应数据
      Map<String, dynamic> data = response.data['data'];
      LoginBean loginInfo = LoginBean.fromJson(data);

      // 3. 保存登录信息
      // 这会自动:
      // - 更新 LoginProvider 中的登录状态
      // - 持久化保存到本地存储
      // - 通知所有监听者
      Provider.of<LoginProvider>(context, listen: false)
          .updateLoginInfo(loginInfo);

      // 4. 登录成功,跳转到主页
      if (mounted) {
        Navigator.of(context).pushReplacementNamed('/home');
      }
    } catch (e) {
      // 处理登录失败
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('登录失败:$e')),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('登录')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              controller: _phoneController,
              decoration: const InputDecoration(labelText: '手机号'),
            ),
            TextField(
              controller: _codeController,
              decoration: const InputDecoration(labelText: '验证码'),
            ),
            ElevatedButton(
              onPressed: _handleLogin,
              child: const Text('登录'),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _phoneController.dispose();
    _codeController.dispose();
    super.dispose();
  }
}

 11. 在其他页面使用登录状态示例

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app/providers/login_provider.dart';

class ProfilePage extends StatelessWidget {
  const ProfilePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('个人中心')),
      body: Consumer<LoginProvider>(
        // Consumer 会自动监听 LoginProvider 的变化
        builder: (context, loginProvider, child) {
          // 根据登录状态显示不同内容
          if (loginProvider.isLogin) {
            return Column(
              children: [
                Text('用户ID: ${loginProvider.loginInfo?.userId}'),
                Text('用户编号: ${loginProvider.loginInfo?.userNo}'),
                ElevatedButton(
                  onPressed: () {
                    // 退出登录
                    loginProvider.logout();
                  },
                  child: const Text('退出登录'),
                ),
              ],
            );
          } else {
            return const Center(
              child: Text('请先登录'),
            );
          }
        },
      ),
    );
  }
}

 12. 监听登录状态变化事件(可选)

import 'package:event_bus/event_bus.dart';
import 'package:your_app/events/event_bus.dart';
import 'package:your_app/events/login_event.dart';

class SomeWidget extends StatefulWidget {
  const SomeWidget({super.key});

  @override
  State<SomeWidget> createState() => _SomeWidgetState();
}

class _SomeWidgetState extends State<SomeWidget> {
  // 事件订阅
  late StreamSubscription<LoginEvent> _loginSubscription;

  @override
  void initState() {
    super.initState();
    
    // 监听登录状态变化事件
    _loginSubscription = eventBus.on<LoginEvent>().listen((event) {
      // 当登录状态变化时,执行相应操作
      if (event.isLogin) {
        print('用户已登录');
        // 执行登录后的操作,例如:刷新数据、更新UI等
      } else {
        print('用户已退出登录');
        // 执行退出登录后的操作,例如:清除数据、跳转登录页等
      }
      
      // 如果需要更新 UI,调用 setState
      if (mounted) {
        setState(() {});
      }
    });
  }

  @override
  void dispose() {
    // 取消事件订阅,避免内存泄漏
    _loginSubscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      // ... UI 代码
    );
  }
}

 

使用流程总结

1. 登录流程 

用户输入账号密码
    ↓
发送登录请求
    ↓
服务器返回登录信息(包含 token)
    ↓
调用 LoginProvider.updateLoginInfo(loginInfo)
    ↓
自动保存到本地存储(UserUtils.save)
    ↓
触发状态更新(notifyListeners)
    ↓
发送登录事件(eventBus.fire,可选)
    ↓
UI 自动更新(Consumer 自动重建)
 
 
 
 
 
 
 
 
 

2. 应用启动流程 

应用启动
    ↓
LoginProvider 初始化
    ↓
自动从本地存储读取登录信息(UserUtils.getLoginInfo)
    ↓
恢复登录状态
    ↓
后续网络请求自动携带 Token
 
 
 
 
 
 
 
 
 

3. 网络请求流程

发起网络请求
    ↓
TokenInterceptor 拦截请求
    ↓
检查是否已登录(LoginProvider.isLogin)
    ↓
如果已登录,在请求头添加 Token
    ↓
发送请求
 
 
 
 
 
 
 
 
 

关键点说明

  1. 单例模式:UserUtils 和 LoginProvider 使用单例,确保全局状态一致
  1. 持久化存储:使用 SharedPreferences(通过 flustars)持久化登录信息
  1. 自动恢复:应用启动时自动从本地存储恢复登录状态
  1. 自动添加 Token:通过 Dio 拦截器自动在请求头添加 Token
  1. 状态管理:使用 Provider 管理登录状态,支持全局访问和自动更新
  1. 事件通知:使用 EventBus 通知登录状态变化(可选)

注意事项

  1. 修改包名:将所有 your_app 替换为你的实际包名
  1. Token 字段名:根据后端要求修改 TokenInterceptor 中的字段名(Access_token)
  1. 错误处理:根据实际需求完善错误处理逻辑
  1. 安全性:Token 存储在 SharedPreferences 中,对于敏感应用可以考虑加密存储
  1. Token 过期:可以在 TokenInterceptor 的 onResponse 中处理 Token 过期情况

扩展功能

  1. Token 刷新:在拦截器中检测 Token 过期,自动刷新 Token
  1. 加密存储:对敏感信息进行加密后再存储
  1. 多账号支持:扩展支持多个账号切换
  1. 自动登录:记住密码功能(需要额外存储密码,注意安全性)