Flutter如何存储Cookie并实现用户登录状态鉴权
Dart 官方http包默认不会自动持久化、自动携带服务端通过Set-Cookie响应头下发的Cookie。Postman测试正常是因为Postman内置了Cookie自动管理机制,你当前的Flutter登录逻辑只解析了响应体内容,完全没有读取响应头中的Cookie字段,自然无法在本地保存Token,后续请求也不会携带对应鉴权信息。
1. 引入必要依赖
放弃用默认的单次http客户端,改用支持Cookie自动管理的请求组件,在pubspec.yaml的dependencies节点添加以下依赖:
dependencies: flutter: sdk: flutter dio: ^5.4.0 dio_cookie_manager: ^3.1.1 path_provider: ^2.1.2 shared_preferences: ^2.2.2
执行flutter pub get完成安装。
2. 服务端跨域配置调整
因为APP请求属于跨域场景,要让Cookie能正常在客户端和服务端之间传递,需要给Express服务端添加跨域配置,先执行npm i cors安装cors中间件,在Express实例初始化的位置添加以下配置:
const cors = require('cors'); app.use(cors({ origin: true, // 生产环境替换为正式请求源,调试阶段设为true可动态匹配 credentials: true // 允许跨域请求携带Cookie }))
同时补全你之前空实现的/authenticate路由逻辑,Token有效时返回200状态码,鉴权失败返回401:
router.get("/authenticate" ,authenticateToken, (req,res)=>{ res.status(200).json({success: true, msg: "有效登录状态"}) })
3. 全局配置支持Cookie持久化的网络客户端
不要每次发请求都新建客户端实例,否则Cookie上下文会丢失,新建全局网络工具类统一管理请求:
import 'package:dio/dio.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:path_provider/path_provider.dart'; import 'dart:io'; class HttpUtil { static late Dio dio; static Future<void> init() async { dio = Dio(BaseOptions( baseUrl: 'http://192.168.8.23:3000', connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), validateStatus: (status) => status! < 500, )); // 配置Cookie本地持久化存储路径 Directory appDocDir = await getApplicationDocumentsDirectory(); String cookieSavePath = '${appDocDir.path}/app_cookies/'; // 添加Cookie管理拦截器,自动处理Cookie的存储、携带、过期清理 dio.interceptors.add(CookieManager(PersistCookieJar( storage: FileStorage(cookieSavePath), ignoreExpires: false, ))); } }
在APP入口main.dart的main函数中先完成网络客户端初始化,再启动APP:
void main() async { WidgetsFlutterBinding.ensureInitialized(); await HttpUtil.init(); runApp(const MyApp()); }
4. 改造登录逻辑
用全局dio实例发登录请求即可,不需要手动解析、存储Cookie,Cookie拦截器会自动识别响应头中的Set-Cookie字段,把access-token持久化到本地,后续所有通过该dio实例发送的请求,都会自动携带有效Cookie,不需要手动加请求头。
修正后的登录方法:
// 注意:_absorbme要定义在State类顶层,不要写在setState内部,否则是局部变量不生效 bool _absorbme = false; Future<void> _signIn(email, password) async { try { setState(() { LoadingWidget.show(context); _absorbme = true; }); final response = await HttpUtil.dio.post( "/route/login", data: <String, String>{ 'email': email.toLowerCase(), 'password': password, }, ); SharedPreferences prefs = await SharedPreferences.getInstance(); final parse = response.data; await prefs.setString('msg', parse["msg"]); await prefs.setBool('loginSuccess', parse["success"] == 'true'); if (parse["success"] == 'true') { if (mounted) Navigator.pushReplacementNamed(context, '/home'); } else { if (mounted) ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(parse["msg"])) ); } } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("请求失败,请检查网络连接")) ); } finally { if (mounted) { setState(() { LoadingWidget.hide(context); _absorbme = false; }); } } }
5. 实现登录状态鉴权流程
添加启动splash页,APP启动时先请求/route/authenticate接口校验本地Cookie是否有效,根据返回结果跳转对应页面:
- 接口返回200:Cookie内Token有效,直接跳转主页
- 接口返回401/403/请求失败:Token过期或未登录,跳转登录页
示例启动页代码:
class SplashPage extends StatefulWidget { const SplashPage({super.key}); @override State<SplashPage> createState() => _SplashPageState(); } class _SplashPageState extends State<SplashPage> { @override void initState() { super.initState(); _checkAuthStatus(); } Future<void> _checkAuthStatus() async { try { final res = await HttpUtil.dio.get("/route/authenticate"); if (res.statusCode == 200 && mounted) { Navigator.pushReplacementNamed(context, '/home'); return; } if (mounted) Navigator.pushReplacementNamed(context, '/login'); } catch (e) { if (mounted) Navigator.pushReplacementNamed(context, '/login'); } } @override Widget build(BuildContext context) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } }
6. 登出逻辑实现
直接调用服务端的登出接口即可,服务端会清除Cookie,客户端的Cookie管理器会自动同步清理失效的Cookie,登出成功后跳转登录页:
Future<void> _handleLogout() async { try { await HttpUtil.dio.get("/route/logout"); if (mounted) Navigator.pushReplacementNamed(context, '/login'); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("登出失败,请重试")) ); } }
- 如果你坚持使用官方
http包,需要手动从response.headers['set-cookie']字段解析Cookie字符串,通过SharedPreferences存储,之后每次发请求手动在headers中添加'cookie': 存储的Cookie字符串,但这种方式需要手动处理Cookie过期、多Cookie拼接、域名匹配等逻辑,出错概率高,不推荐。 - Android模拟器访问本地电脑的Node服务时,若用127.0.0.1会指向模拟器自身,需要替换为
10.0.2.2,你当前使用的局域网IP只要手机和电脑在同一网段可正常访问。 - 不要在Flutter端尝试用Web端逻辑读取httpOnly Cookie,Flutter是原生请求栈,直接通过响应头读取Set-Cookie是合法操作,不存在浏览器端的跨域读取限制。
内容的提问来源于stack exchange,提问作者CINO

