在 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
一、核心思路梳理
- 登录状态存储 :用本地持久化(如
SharedPreferences)保存登录凭证(token / 用户信息),APP 启动时读取并初始化状态; - 全局状态管理 :用
Provider/GetX/Bloc管理登录状态,让所有页面能实时感知登录状态变化; - 路由权限拦截:自定义路由守卫,跳转需要登录的页面时,检查登录状态,未登录则跳转到登录页;
- 接口鉴权:统一封装网络请求,请求头自动携带 token,token 失效时(401 错误)触发登出逻辑;
- 登出逻辑:清除本地凭证 + 重置全局状态 + 跳转登录页。
二、分步实现(以 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:路由权限拦截(自定义路由守卫)
自定义 MaterialApp 的 onGenerateRoute,实现「需要登录的页面拦截」:
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('启动中...')),
);
}
}
四、核心总结
- 状态持久化 :用
SharedPreferences/flutter_secure_storage存 token,APP 启动时初始化; - 全局状态:用 Provider/GetX/Bloc 管理登录状态,所有页面实时感知;
- 路由拦截 :自定义
onGenerateRoute,拦截需要登录的页面,未登录则跳转登录页; - 接口鉴权:统一封装网络请求,自动携带 token,处理 401 失效逻辑;
- 登出逻辑:清除本地凭证 + 重置状态 + 跳转登录页(清空路由栈)。
这套方案覆盖了从「登录」到「权限控制」再到「登出」的全流程,适配大多数 Flutter 应用的权限管理需求,可根据项目复杂度(如多角色权限、动态路由)扩展。
分享
如何使用SharedPreferences保存用户登录凭证?
如何使用Provider管理登录状态?
如何自定义路由守卫?