flutter如何实现有登陆权限管理

在 Flutter 中实现登录权限管理,核心是解决「登录状态持久化」「路由权限拦截」「接口鉴权」三大核心问题。以下是一套完整、可落地的实现方案,包含状态管理、路由拦截、持久化、接口鉴权等关键环节:

git: github.com/winfrise/fl...

项目目录:

plaintext 复制代码
lib/
├── main.dart                 # 项目入口
├── pages/                    # 页面(登录页、首页、个人中心等)
│   ├── login/
│   ├── home/
│   └── profile/
├── providers/                # 状态管理(Provider/ChangeNotifier 类)
│   ├── auth_provider.dart    # 登录状态管理(核心文件)
│   └── other_provider.dart   # 其他全局状态(如主题、设置)
├── routes/                   # 路由相关(路由常量、路由拦截)
│   ├── app_routes.dart
│   └── app_router.dart
├── utils/                    # 工具类(网络请求、本地存储、通用方法)
│   ├── http_util.dart
│   └── storage_util.dart
└── models/                   # 数据模型(用户信息、接口返回模型)
    └── user_model.dart

一、核心思路梳理

  1. 登录状态存储 :用本地持久化(如 SharedPreferences)保存登录凭证(token / 用户信息),APP 启动时读取并初始化状态;
  2. 全局状态管理 :用 Provider/GetX/Bloc 管理登录状态,让所有页面能实时感知登录状态变化;
  3. 路由权限拦截:自定义路由守卫,跳转需要登录的页面时,检查登录状态,未登录则跳转到登录页;
  4. 接口鉴权:统一封装网络请求,请求头自动携带 token,token 失效时(401 错误)触发登出逻辑;
  5. 登出逻辑:清除本地凭证 + 重置全局状态 + 跳转登录页。

二、分步实现(以 Provider + SharedPreferences 为例)

步骤 1:依赖准备

添加核心依赖(持久化 + 状态管理):

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.2.2  # 本地持久化
  provider: ^6.1.1            # 状态管理
  dio: ^5.4.0                  # 网络请求(接口鉴权)
  flutter_secure_storage: ^8.0.0 # 敏感信息加密存储(可选,替代 SharedPreferences 存 token)

步骤 2:封装登录状态管理(全局状态)

创建 AuthProvider 管理登录状态,包含「登录 / 登出 / 状态检查」核心方法:

dart 复制代码
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

// 登录状态模型(可扩展用户信息)
class UserModel {
  final String token;
  final String username;

  UserModel({required this.token, required this.username});
}

// 全局登录状态管理
class AuthProvider extends ChangeNotifier {
  UserModel? _user; // 当前登录用户(null 表示未登录)
  bool _isLoading = false; // 登录/登出加载状态

  UserModel? get user => _user;
  bool get isLogin => _user != null;
  bool get isLoading => _isLoading;

  // APP 启动时初始化登录状态(读取本地存储)
  Future<void> initAuth() async {
    final sp = await SharedPreferences.getInstance();
    final token = sp.getString('token');
    final username = sp.getString('username');
    if (token != null && username != null) {
      _user = UserModel(token: token, username: username);
    }
    notifyListeners();
  }

  // 登录方法(存储凭证到本地 + 更新状态)
  Future<void> login({required String username, required String password}) async {
    _isLoading = true;
    notifyListeners();

    // 模拟接口请求(替换为真实登录接口)
    await Future.delayed(const Duration(seconds: 1));
    final token = 'mock_token_${DateTime.now().millisecondsSinceEpoch}';

    // 存储到本地
    final sp = await SharedPreferences.getInstance();
    await sp.setString('token', token);
    await sp.setString('username', username);

    // 更新全局状态
    _user = UserModel(token: token, username: username);
    _isLoading = false;
    notifyListeners();
  }

  // 登出方法(清除本地凭证 + 重置状态)
  Future<void> logout() async {
    _isLoading = true;
    notifyListeners();

    // 清除本地存储
    final sp = await SharedPreferences.getInstance();
    await sp.remove('token');
    await sp.remove('username');

    // 重置状态
    _user = null;
    _isLoading = false;
    notifyListeners();
  }
}

步骤 3:路由权限拦截(自定义路由守卫)

自定义 MaterialApponGenerateRoute,实现「需要登录的页面拦截」:

3.1 定义路由常量(区分需要登录的页面)
dart 复制代码
// 路由常量
class AppRoutes {
  // 公开页面(无需登录)
  static const login = '/login';
  static const splash = '/splash';

  // 需登录页面
  static const home = '/home';
  static const profile = '/profile';
  static const settings = '/settings';

  // 判断页面是否需要登录
  static bool needAuth(String routeName) {
    return [home, profile, settings].contains(routeName);
  }
}
3.2 自定义路由生成器(拦截逻辑)
dart 复制代码
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// 页面导入(示例)
import 'pages/splash_page.dart';
import 'pages/login_page.dart';
import 'pages/home_page.dart';
import 'pages/profile_page.dart';

class AppRouter {
  static Route<dynamic> generateRoute(RouteSettings settings) {
    final routeName = settings.name ?? AppRoutes.splash;

    return MaterialPageRoute(
      builder: (context) {
        // 1. 检查页面是否需要登录
        final needAuth = AppRoutes.needAuth(routeName);
        final authProvider = Provider.of<AuthProvider>(context, listen: false);

        // 2. 未登录且页面需要登录 → 跳转到登录页(携带原路由,登录后返回)
        if (needAuth && !authProvider.isLogin) {
          return LoginPage(
            redirectRoute: routeName, // 登录成功后跳转的目标路由
          );
        }

        // 3. 路由分发
        switch (routeName) {
          case AppRoutes.splash:
            return const SplashPage();
          case AppRoutes.login:
            return LoginPage(redirectRoute: settings.arguments as String?);
          case AppRoutes.home:
            return const HomePage();
          case AppRoutes.profile:
            return const ProfilePage();
          default:
            return const Scaffold(body: Center(child: Text('404 Page')));
        }
      },
      settings: settings,
    );
  }
}

步骤 4:封装网络请求(接口鉴权)

统一处理 token 携带、401 失效拦截:

dart 复制代码
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/material.dart';
import 'auth_provider.dart';

class HttpUtil {
  static late Dio _dio;

  // 初始化 Dio
  static void init() {
    _dio = Dio(BaseOptions(
      baseUrl: 'https://your-api-domain.com',
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 10),
    ));

    // 添加请求拦截器:自动携带 token
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) async {
        final sp = await SharedPreferences.getInstance();
        final token = sp.getString('token');
        if (token != null) {
          options.headers['Authorization'] = 'Bearer $token';
        }
        return handler.next(options);
      },
      onError: (error, handler) async {
        // 401 错误:token 失效 → 触发登出
        if (error.response?.statusCode == 401) {
          // 获取全局 AuthProvider
          final context = navigatorKey.currentContext!;
          final authProvider = Provider.of<AuthProvider>(context, listen: false);
          await authProvider.logout();
          // 跳转到登录页
          Navigator.pushNamedAndRemoveUntil(
            context,
            AppRoutes.login,
            (route) => false,
          );
        }
        return handler.next(error);
      },
    ));
  }

  // 封装 GET 请求
  static Future<Response> get(String path, {Map<String, dynamic>? params}) async {
    return await _dio.get(path, queryParameters: params);
  }

  // 封装 POST 请求
  static Future<Response> post(String path, {dynamic data}) async {
    return await _dio.post(path, data: data);
  }
}

// 全局导航 key(用于无上下文时跳转)
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

步骤 5:初始化全局状态 & 路由

main.dart 中初始化状态管理、路由、网络请求:

dart

typescript 复制代码
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'auth_provider.dart';
import 'app_router.dart';
import 'http_util.dart';
import 'app_routes.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // 初始化网络请求
  HttpUtil.init();

  runApp(
    ChangeNotifierProvider(
      create: (context) => AuthProvider()..initAuth(), // 启动时初始化登录状态
      child: const MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '权限管理示例',
      navigatorKey: navigatorKey, // 全局导航 key
      initialRoute: AppRoutes.splash, // 启动页
      onGenerateRoute: AppRouter.generateRoute, // 自定义路由生成器
      debugShowCheckedModeBanner: false,
    );
  }
}

步骤 6:实现登录 / 登出页面示例

6.1 登录页(LoginPage)
dart 复制代码
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'auth_provider.dart';
import 'app_routes.dart';

class LoginPage extends StatefulWidget {
  final String? redirectRoute; // 登录成功后跳转的路由

  const LoginPage({super.key, this.redirectRoute});

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

class _LoginPageState extends State<LoginPage> {
  final _usernameCtrl = TextEditingController();
  final _passwordCtrl = TextEditingController();

  @override
  Widget build(BuildContext context) {
    final authProvider = Provider.of<AuthProvider>(context);

    return Scaffold(
      appBar: AppBar(title: const Text('登录')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              controller: _usernameCtrl,
              decoration: const InputDecoration(hintText: '用户名'),
            ),
            TextField(
              controller: _passwordCtrl,
              obscureText: true,
              decoration: const InputDecoration(hintText: '密码'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: authProvider.isLoading
                  ? null
                  : () async {
                      // 调用登录方法
                      await authProvider.login(
                        username: _usernameCtrl.text,
                        password: _passwordCtrl.text,
                      );
                      // 登录成功 → 跳转到目标路由(默认首页)
                      if (authProvider.isLogin) {
                        Navigator.pushReplacementNamed(
                          context,
                          widget.redirectRoute ?? AppRoutes.home,
                        );
                      }
                    },
              child: authProvider.isLoading
                  ? const CircularProgressIndicator(size: 20)
                  : const Text('登录'),
            ),
          ],
        ),
      ),
    );
  }
}
6.2 个人中心(需登录,示例)
dart 复制代码
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'auth_provider.dart';
import 'app_routes.dart';

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

  @override
  Widget build(BuildContext context) {
    final authProvider = Provider.of<AuthProvider>(context);
    final user = authProvider.user!;

    return Scaffold(
      appBar: AppBar(title: const Text('个人中心')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('用户名:${user.username}'),
            Text('Token:${user.token}'),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () async {
                await authProvider.logout();
                // 登出后跳转到登录页
                Navigator.pushNamedAndRemoveUntil(
                  context,
                  AppRoutes.login,
                  (route) => false,
                );
              },
              child: const Text('退出登录'),
            ),
          ],
        ),
      ),
    );
  }
}

三、进阶优化点

1. 敏感信息加密存储

SharedPreferences 是明文存储,token 等敏感信息建议用 flutter_secure_storage(基于 Keychain/iOS、Keystore/Android 加密):

dart

php 复制代码
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final storage = const FlutterSecureStorage();

// 存储 token
await storage.write(key: 'token', value: token);
// 读取 token
final token = await storage.read(key: 'token');
// 清除 token
await storage.delete(key: 'token');

2. 多状态管理方案(替代 Provider)

  • GetX :更轻量,无需上下文,直接通过 Get.put(AuthController()) 管理状态,路由拦截用 GetMiddleware
  • Bloc/Cubit :适合复杂状态逻辑,通过 BlocBuilder 响应状态变化,路由拦截结合 BlocListener
  • Riverpod:Provider 的升级版,更灵活的依赖注入,无上下文访问状态。

3. 自动刷新 Token

接口返回 401 时,可先尝试用 refreshToken 刷新 token,失败再登出:

dart

dart 复制代码
onError: (error, handler) async {
  if (error.response?.statusCode == 401) {
    // 1. 获取 refreshToken
    final refreshToken = await storage.read(key: 'refreshToken');
    if (refreshToken == null) {
      // 无 refreshToken → 登出
      await authProvider.logout();
      return handler.reject(error);
    }

    // 2. 调用刷新 token 接口
    try {
      final res = await _dio.post('/refreshToken', data: {'refreshToken': refreshToken});
      final newToken = res.data['token'];
      // 3. 存储新 token
      await storage.write(key: 'token', value: newToken);
      // 4. 重新发起原请求
      final options = error.requestOptions;
      options.headers['Authorization'] = 'Bearer $newToken';
      final retryRes = await _dio.request(
        options.path,
        options: options,
      );
      return handler.resolve(retryRes);
    } catch (e) {
      // 刷新失败 → 登出
      await authProvider.logout();
      return handler.reject(error);
    }
  }
  return handler.next(error);
}

4. 启动页判断登录状态

Splash 页初始化时,根据登录状态决定跳转到首页 / 登录页:

dart

scala 复制代码
class SplashPage extends StatefulWidget {
  const SplashPage({super.key});

  @override
  State<SplashPage> createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> {
  @override
  void initState() {
    super.initState();
    _checkAuth();
  }

  Future<void> _checkAuth() async {
    await Future.delayed(const Duration(seconds: 2)); // 模拟启动页延迟
    final authProvider = Provider.of<AuthProvider>(context, listen: false);
    if (authProvider.isLogin) {
      Navigator.pushReplacementNamed(context, AppRoutes.home);
    } else {
      Navigator.pushReplacementNamed(context, AppRoutes.login);
    }
  }

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(child: Text('启动中...')),
    );
  }
}

四、核心总结

  1. 状态持久化 :用 SharedPreferences/flutter_secure_storage 存 token,APP 启动时初始化;
  2. 全局状态:用 Provider/GetX/Bloc 管理登录状态,所有页面实时感知;
  3. 路由拦截 :自定义 onGenerateRoute,拦截需要登录的页面,未登录则跳转登录页;
  4. 接口鉴权:统一封装网络请求,自动携带 token,处理 401 失效逻辑;
  5. 登出逻辑:清除本地凭证 + 重置状态 + 跳转登录页(清空路由栈)。

这套方案覆盖了从「登录」到「权限控制」再到「登出」的全流程,适配大多数 Flutter 应用的权限管理需求,可根据项目复杂度(如多角色权限、动态路由)扩展。

分享

如何使用SharedPreferences保存用户登录凭证?

如何使用Provider管理登录状态?

如何自定义路由守卫?

相关推荐
ohyeah1 小时前
深入理解 JavaScript 中的继承与 instanceof 原理
前端·javascript
crary,记忆1 小时前
React 之 useEffect
前端·javascript·学习·react.js
BD_Marathon1 小时前
【JavaWeb】HTML常见标签——标题段落和换行
前端·html
小飞侠在吗1 小时前
vue OptionsAPI与CompositionAPI
前端·javascript·vue.js
天涯路s1 小时前
qt怎么将模块注册成插件
java·服务器·前端·qt
只与明月听1 小时前
FastAPI入门实战
前端·后端·python
脾气有点小暴1 小时前
Tree Shaking 深度解析:原理、应用与实践
前端·vue.js
一点一木2 小时前
🚀 2025 年 11 月 GitHub 十大热门项目排行榜 🔥
前端·人工智能·github
白粥2 小时前
HTML标题标签<h1>到<h6>
前端·html