Flutter Web升级空安全后刷新页面自动登出问题求助
Flutter Web升级空安全后刷新自动登出问题
我的Flutter Web应用在未升级空安全前运行正常,升级后出现刷新页面即自动登出的问题。以下是相关代码:
Auth类代码
import 'dart:convert'; import 'dart:async'; import 'package:jwt_decode/jwt_decode.dart' as Jwt; import 'package:flutter/widgets.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import '../main.dart'; import '../models/http_exception.dart'; class Auth with ChangeNotifier { String? _token; DateTime? _expiryDate; Timer? _authTimer; bool get isAuth { return token != null; } String? get token { if (_expiryDate != null && _expiryDate!.isAfter(DateTime.now()) && _token != null) { return _token; } return null; } Future<void> _authenticate(String email, String password) async { final url = (APIPROTOCOL == 'http://') ? APIPROTOCOL + APIHOST + '/api/token/' : APIPROTOCOL + APIHOST + APIHOST_PREFIX + '/api/token/'; try { final response = await http.post( Uri.parse(url), headers: {"Content-Type": "application/json"}, body: json.encode( { 'username': email, 'password': password, }, ), ); final responseData = json.decode(response.body); if (responseData['detail'] != null) { throw HttpException(responseData['detail']); } _token = responseData['access']; final parsedJwt = Jwt.Jwt.parseJwt(_token!); _expiryDate = DateTime.fromMillisecondsSinceEpoch(parsedJwt['exp'] * 1000); _autoLogout(); notifyListeners(); final prefs = await SharedPreferences.getInstance(); final userData = json.encode( { 'token': _token, 'expiryDate': _expiryDate?.toIso8601String(), }, ); prefs.setString('userData', userData); } catch (error) { print(error); throw error; } } Future<void> signup(String email, String password) async { return _authenticate(email, password); } Future<void> login(String email, String password) async { return _authenticate(email, password); } Future<bool> tryAutoLogin() async { final prefs = await SharedPreferences.getInstance(); if (!prefs.containsKey('userData')) { return false; } final extractedUserData = json.decode(prefs.getString('userData') as String) as Map<String, Object>; final expiryDate = DateTime.parse(extractedUserData['expiryDate'] as String); if (expiryDate.isBefore(DateTime.now())) { return false; } _token = extractedUserData['token'] as String; _expiryDate = expiryDate; notifyListeners(); _autoLogout(); return true; } Future<void> logout() async { _token = null; _expiryDate = null; if (_authTimer != null) { _authTimer!.cancel(); _authTimer = null; } notifyListeners(); final prefs = await SharedPreferences.getInstance(); // prefs.remove('userData'); prefs.clear(); } void _autoLogout() { if (_authTimer != null) { _authTimer!.cancel(); } final timeToExpiry = _expiryDate!.difference(DateTime.now()).inSeconds; _authTimer = Timer(Duration(seconds: timeToExpiry), logout); } }
登录表单代码
class _SignUpFormState extends State<SignUpForm> { final GlobalKey<FormState> _formKey = GlobalKey(); final _passwordTextController = TextEditingController(); final _usernameTextController = TextEditingController(); Map<String, String> _authData = { 'email': '', 'password': '', }; double _formProgress = 0; var _isLoading = false; @override void dispose() { _passwordTextController.dispose(); _usernameTextController.dispose(); super.dispose(); } void _showErrorDialog(String message) { showDialog( context: context, builder: (ctx) => AlertDialog( title: Text('Achtung...'), content: Text(message), actions: <Widget>[ TextButton( child: Text('Okay'), onPressed: () { Navigator.of(ctx).pop(); }, ) ], ), ); } Future<void> _submit() async { if (!_formKey.currentState!.validate()) { // Invalid! return; } _formKey.currentState?.save(); setState(() { _isLoading = true; }); try { // Log user in await Provider.of<Auth>(context, listen: false).login( _authData['email']!, _authData['password']!, ); } on HttpException catch (error) { var errorMessage = error.toString(); _showErrorDialog(errorMessage); } catch (error) { const errorMessage = 'Wir konnten dich nicht anmelden. Bitte überprüfe deine Verbindung.'; _showErrorDialog(errorMessage); } setState(() { _isLoading = false; }); } @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ LinearProgressIndicator( value: _formProgress, backgroundColor: Theme.of(context).accentColor, ), SizedBox( height: 12, ), Text('Anmeldung', style: Theme.of(context).textTheme.headline4), Padding( padding: EdgeInsets.all(24.0), child: TextFormField( controller: _usernameTextController, decoration: InputDecoration(hintText: 'Email-Adresse'), validator: (value) { if (value!.isEmpty) { return 'Bitte Visoon Email-Adresse eingeben'; } return null; }, onSaved: (value) { _authData['email'] = value!; }, ), ), Padding( padding: EdgeInsets.symmetric(horizontal: 24.0), child: TextFormField( controller: _passwordTextController, obscureText: true, decoration: InputDecoration(hintText: 'Passwort'), validator: (value) { if (value!.isEmpty) { return 'Bitte Passwort eingeben'; } return null; }, onSaved: (value) { _authData['password'] = value!; }, ), ), SizedBox( height: 12, ), if (_isLoading) CircularProgressIndicator() else TextButton( style: TextButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.secondary, primary: Colors.white), onPressed: _submit, child: Text( 'Anmelden', style: TextStyle(fontSize: 18), ), ), SizedBox( height: 12, ), ], ), ); } }
主应用中tryAutoLogin调用代码
class VisoonApp extends StatelessWidget { const VisoonApp({super.key}); @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider.value( value: Auth(), ), ], child: Consumer<Auth>( builder: (ctx, auth, _) => MaterialApp( scrollBehavior: CustomScrollBehavior(), title: 'Visoon Forecasting', theme: ThemeData( primaryColor: const Color(0xff1e1e1e), fontFamily: 'Futura', colorScheme: ColorScheme.fromSwatch() .copyWith(secondary: const Color(0xffe20644)), ), navigatorObservers: [routeObserver], home: auth.isAuth ? DashboardScreen() : FutureBuilder( future: auth.tryAutoLogin(), builder: (ctx, authResultSnapshot) => authResultSnapshot.connectionState == ConnectionState.waiting ? SplashScreen() : SignUpScreen(), ),
问题原因
tryAutoLogin类型转换风险:强制将extractedUserData['token']和extractedUserData['expiryDate']转为非空String,若存储的数据为空安全升级前的null值,会触发类型转换错误,导致自动登录失败返回false,页面跳转至登录页。_autoLogout非空断言隐患:直接使用_expiryDate!,若_expiryDate为null(比如自动登录失败时),会抛出异常,进而触发logout方法清空状态。- Provider初始化方式错误:
ChangeNotifierProvider.value(value: Auth())会在Widget重建时重新创建Auth实例,Web刷新后状态丢失,结合自动登录的问题,直接回到登录状态。
修复方案
1. 安全处理自动登录的类型转换
修改tryAutoLogin方法,增加空值检查,避免强制转换失败:
Future<bool> tryAutoLogin() async { final prefs = await SharedPreferences.getInstance(); if (!prefs.containsKey('userData')) { return false; } final userDataString = prefs.getString('userData'); if (userDataString == null) { return false; } final extractedUserData = json.decode(userDataString) as Map<String, dynamic>; final expiryDateStr = extractedUserData['expiryDate'] as String?; if (expiryDateStr == null) { return false; } final expiryDate = DateTime.parse(expiryDateStr); if (expiryDate.isBefore(DateTime.now())) { return false; } final token = extractedUserData['token'] as String?; if (token == null) { return false; } _token = token; _expiryDate = expiryDate; notifyListeners(); _autoLogout(); return true; }
2. 修复_autoLogout的空安全问题
增加_expiryDate空值检查,避免非空断言报错:
void _autoLogout() { if (_authTimer != null) { _authTimer!.cancel(); _authTimer = null; } if (_expiryDate == null) return; final timeToExpiry = _expiryDate!.difference(DateTime.now()).inSeconds; if (timeToExpiry <= 0) { logout(); return; } _authTimer = Timer(Duration(seconds: timeToExpiry), logout); }
3. 修正Provider创建方式
使用create方法初始化Auth,确保全局仅一个实例:
MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => Auth()), ], // 剩余代码不变 )
4. 确保存储数据的非空性
在_authenticate方法中,存储用户数据时确保token和expiryDate非空:
final userData = json.encode( { 'token': _token!, // 此处_token已通过JWT解析赋值,非空 'expiryDate': _expiryDate!.toIso8601String(), // 同理_expiryDate非空 }, );
内容的提问来源于stack exchange,提问作者Davood
相关产品推荐
相关产品推荐

