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管理登录状态?

如何自定义路由守卫?

相关推荐
恋猫de小郭27 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端