5.2Stockage sécurisé avec Secure Storage
5.2.1 – Pourquoi sécuriser certaines données
🔒 Qu'est-ce qu'une donnée sensible ?
Une donnée sensible est une information qui, si elle est compromise, peut causer des préjudices à l'utilisateur ou à l'application. Ces données nécessitent une protection particulière.
Voici des exemples de données sensibles :
- Mots de passe : Les mots de passe ne doivent jamais être stockés en clair
- Tokens d'authentification : JWT, OAuth tokens, API keys
- Informations bancaires : Numéros de carte, codes CVV (même si c'est rare dans une app mobile)
- Données personnelles : Numéros de sécurité sociale, informations médicales
- Clés de chiffrement : Clés privées, secrets d'application
⚠️ Pourquoi SharedPreferences n'est pas suffisant ?
SharedPreferences stocke les données en clair (non chiffrées). Cela signifie que :
- Sur Android : Les données sont dans des fichiers XML lisibles
- Sur iOS : Les données sont dans NSUserDefaults, accessibles si l'appareil est jailbreaké
- Sur Web : Les données sont dans localStorage, accessibles via les outils de développement
Si vous stockez un token d'authentification dans SharedPreferences et que quelqu'un accède au système de fichiers (root sur Android, jailbreak sur iOS), il peut lire votre token et se faire passer pour vous. C'est un risque majeur de sécurité !
✅ Comment Secure Storage protège vos données
flutter_secure_storage utilise le stockage sécurisé natif de chaque plateforme :
- Android : Utilise
EncryptedSharedPreferences(chiffrement AES-256) ouKeyStorepour les clés - iOS : Utilise
Keychain(chiffrement au niveau du système) - Web : Utilise le chiffrement avec des clés dérivées
Le chiffrement transforme vos données en texte illisible. Même si quelqu'un accède aux fichiers, il ne peut pas lire les données sans la clé de déchiffrement, qui est gérée de manière sécurisée par le système d'exploitation.
📊 Tableau comparatif : SharedPreferences vs Secure Storage
Tableau comparatif entre SharedPreferences et Secure Storage.
🤔 Quand utiliser Secure Storage ?
✅ Utilisez Secure Storage pour :
- Mots de passe
- Tokens d'authentification (JWT, OAuth)
- API keys et secrets
- Clés de chiffrement
- Toute donnée confidentielle
❌ Utilisez SharedPreferences pour :
- Préférences utilisateur (thème, langue)
- Paramètres de l'application
- Données non sensibles
- Compteurs et statistiques
Si la divulgation de la donnée pourrait causer un préjudice (accès non autorisé, vol d'identité, etc.), utilisez Secure Storage. Sinon, SharedPreferences est suffisant et plus performant.
5.2.2 – Installation de flutter_secure_storage
🎯 Objectif
Installer le package flutter_secure_storage dans votre projet Flutter et le configurer correctement.
💡 Installation pas à pas
Étape 1 : Ajouter la dépendance
Ouvrez le fichier pubspec.yaml de votre projet Flutter et ajoutez la dépendance flutter_secure_storage dans la section dependencies :
dependencies:
flutter:
sdk: flutter
flutter_secure_storage: ^9.0.0
La version peut varier. Pour obtenir la dernière version, consultez pub.dev/packages/flutter_secure_storage ou utilisez la commande :
flutter pub add flutter_secure_storage
Cette commande ajoute automatiquement la dernière version compatible.
Étape 2 : Installer le package
Exécutez la commande suivante dans votre terminal, à la racine de votre projet Flutter :
flutter pub get
✅ Résultat attendu : Vous devriez voir un message indiquant que le package a été installé avec succès.
Étape 3 : Configuration Android (optionnel mais recommandé)
Pour Android, vous pouvez configurer le niveau de sécurité minimum dans android/app/build.gradle :
android {
defaultConfig {
minSdkVersion 18 // Au minimum 18 pour flutter_secure_storage
}
}
Sur iOS, aucune configuration supplémentaire n'est nécessaire. Le package utilise automatiquement le Keychain d'iOS, qui est sécurisé par défaut.
Étape 4 : Importer le package
Dans le fichier Dart où vous souhaitez utiliser Secure Storage, ajoutez l'import en haut du fichier :
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
📖 Vérifier l'installation
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
void main() {
print('flutter_secure_storage est prêt à être utilisé !');
}
Si aucune erreur n'apparaît, l'installation est réussie. ✅
Si vous utilisez VS Code ou Android Studio, l'IDE vous proposera automatiquement d'importer le package lorsque vous taperez
FlutterSecureStorage dans votre code.
5.2.3 – Stocker des données sensibles
Pour stocker des données sensibles avec Secure Storage, vous devez créer une instance de FlutterSecureStorage et utiliser la méthode write().
📝 Qu'est-ce que la sauvegarde sécurisée ?
La sauvegarde sécurisée consiste à stocker des données de manière chiffrée. Les données sont automatiquement chiffrées avant d'être écrites sur le disque, et déchiffrées lors de la lecture. Secure Storage gère automatiquement le chiffrement et le déchiffrement pour vous.
📝 Syntaxe
Voici comment créer une instance et sauvegarder des données :
// Créer une instance de FlutterSecureStorage
final storage = FlutterSecureStorage();
// Sauvegarder une donnée (opération asynchrone)
await storage.write(key: 'token', value: 'mon_token_secret');
await storage.write(key: 'password', value: 'mon_mot_de_passe');
Analysons cette syntaxe :
FlutterSecureStorage(): Crée une instance de Secure Storagewrite(key: 'token', value: '...'): Sauvegarde une donnée avec une clé (opération asynchrone)key: Le nom de la clé (comme un identifiant)value: La valeur à sauvegarder (doit être une String)
L'opération de chiffrement et d'écriture sur le disque est asynchrone. C'est pourquoi vous devez utiliser
await et que votre fonction doit être async. 🔄
🧪 Exemple : Sauvegarder un token d'authentification
Voici un exemple complet qui sauvegarde un token d'authentification de manière sécurisée :
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
void main() {
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const MaPage(),
);
}
}
class MaPage extends StatefulWidget {
const MaPage({super.key});
@override
State<MaPage> createState() => _MaPageState();
}
class _MaPageState extends State<MaPage> {
final _tokenController = TextEditingController();
final _storage = FlutterSecureStorage();
Future<void> sauvegarderToken() async {
// Sauvegarder le token de manière sécurisée
await _storage.write(
key: 'auth_token',
value: _tokenController.text,
);
// Afficher un message de confirmation
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Token sauvegardé de manière sécurisée !'),
backgroundColor: Colors.green,
),
);
}
}
@override
void dispose() {
_tokenController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sauvegarder avec Secure Storage'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _tokenController,
decoration: const InputDecoration(
labelText: 'Token d\'authentification',
border: OutlineInputBorder(),
hintText: 'Entrez votre token',
),
obscureText: true, // Masquer le texte saisi
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: sauvegarderToken,
child: const Text('Sauvegarder de manière sécurisée'),
),
],
),
),
),
);
}
}
Dans cet exemple, le token est sauvegardé de manière chiffrée. Même si quelqu'un accède au système de fichiers, il ne pourra pas lire le token en clair.
obscureText: true masque le texte saisi dans le TextField (utile pour les mots de passe et tokens). Les caractères sont remplacés par des points pour éviter qu'ils soient visibles à l'écran.
💾 Options de configuration
Vous pouvez configurer Secure Storage avec des options supplémentaires :
// Configuration avec options
final storage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true, // Utiliser EncryptedSharedPreferences
),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
// Sauvegarder avec des options spécifiques
await storage.write(
key: 'token',
value: 'mon_token',
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
);
N'oubliez pas que les opérations avec Secure Storage sont asynchrones. Vous devez toujours utiliser
await et déclarer votre fonction comme async.💡 À propos de
mounted :Avant d'appeler
setState ou d'afficher un SnackBar après une opération asynchrone, vérifiez toujours if (mounted). Cela permet d'éviter des erreurs si le widget a été retiré de l'arbre Flutter.
5.2.4 – Lire et supprimer des données sécurisées
Pour lire des données sécurisées, vous utilisez la méthode read(). Pour supprimer, vous utilisez delete() ou deleteAll().
📖 Lire des données sécurisées
La méthode read() déchiffre et retourne la valeur stockée. Elle retourne null si la clé n'existe pas.
📝 Syntaxe
// Créer une instance
final storage = FlutterSecureStorage();
// Lire une donnée (retourne null si la clé n'existe pas)
String? token = await storage.read(key: 'auth_token');
// Avec une valeur par défaut
String token = await storage.read(key: 'auth_token') ?? 'Aucun token';
// Lire toutes les clés
Map<String, String> allData = await storage.readAll();
Analysons cette syntaxe :
read(key: 'auth_token'): Lit et déchiffre la valeur associée à la cléreadAll(): Lit toutes les données stockées (retourne une Map)??: Opérateur qui fournit une valeur par défaut si le résultat estnull
🗑️ Supprimer des données sécurisées
Pour supprimer une clé spécifique ou toutes les données :
// Supprimer une clé spécifique
await storage.delete(key: 'auth_token');
// Supprimer toutes les données
await storage.deleteAll();
🧪 Exemple : Lire et supprimer un token
Voici un exemple complet qui lit et supprime un token d'authentification :
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
void main() {
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const MaPage(),
);
}
}
class MaPage extends StatefulWidget {
const MaPage({super.key});
@override
State<MaPage> createState() => _MaPageState();
}
class _MaPageState extends State<MaPage> {
final _storage = FlutterSecureStorage();
final _tokenController = TextEditingController();
String tokenAffiche = 'Aucun token';
@override
void initState() {
super.initState();
chargerToken();
}
// Charger le token au démarrage
Future<void> chargerToken() async {
String? token = await _storage.read(key: 'auth_token');
setState(() {
tokenAffiche = token ?? 'Aucun token sauvegardé';
_tokenController.text = token ?? '';
});
}
// Sauvegarder le token modifié
Future<void> sauvegarderToken() async {
if (_tokenController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Le token ne peut pas être vide'),
backgroundColor: Colors.red,
),
);
return;
}
await _storage.write(
key: 'auth_token',
value: _tokenController.text,
);
chargerToken(); // Recharger l'affichage
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Token sauvegardé'),
backgroundColor: Colors.green,
),
);
}
}
// Supprimer le token
Future<void> supprimerToken() async {
await _storage.delete(key: 'auth_token');
chargerToken(); // Recharger l'affichage
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Token supprimé'),
backgroundColor: Colors.orange,
),
);
}
}
@override
void dispose() {
_tokenController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Lire et supprimer avec Secure Storage'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Token actuel :',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Text(
tokenAffiche,
style: const TextStyle(fontSize: 16),
),
),
const SizedBox(height: 24),
TextField(
controller: _tokenController,
decoration: const InputDecoration(
labelText: 'Modifier le token',
border: OutlineInputBorder(),
hintText: 'Entrez un nouveau token',
),
obscureText: true, // Masquer le texte saisi
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: sauvegarderToken,
child: const Text('Sauvegarder le token'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: chargerToken,
child: const Text('Recharger le token'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: supprimerToken,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text('Supprimer le token'),
),
],
),
),
),
);
}
}
Dans cet exemple :
- Le token est chargé au démarrage de l'application
- L'utilisateur peut recharger le token en cliquant sur le bouton
- L'utilisateur peut supprimer le token, qui sera retiré du stockage sécurisé
Vous pouvez vérifier si une clé existe en essayant de la lire :
String? token = await storage.read(key: 'auth_token');
if (token != null) {
print('Token trouvé : $token');
} else {
print('Aucun token sauvegardé');
}
Comme la méthode
read() peut retourner null, utilisez toujours l'opérateur ?? pour fournir une valeur par défaut. Cela évite les erreurs si la clé n'existe pas encore.
5.2.5 – Différences avec SharedPreferences
Secure Storage et SharedPreferences sont deux solutions de stockage différentes, chacune adaptée à des cas d'usage spécifiques. Comprendre leurs différences vous aidera à choisir la bonne solution.
🔐 Sécurité
Secure Storage
- ✅ Chiffrement AES-256
- ✅ Utilise KeyStore (Android) / Keychain (iOS)
- ✅ Protection au niveau du système
- ✅ Même avec accès root/jailbreak, les données restent protégées
SharedPreferences
- ❌ Pas de chiffrement
- ❌ Données en clair dans les fichiers
- ❌ Accessibles si accès au système de fichiers
- ❌ Ne pas utiliser pour des données sensibles
📊 Types de données
SharedPreferences peut stocker plusieurs types :
StringintdoubleboolList<String>
Secure Storage ne peut stocker que :
Stringuniquement
Secure Storage chiffre les données, et le chiffrement fonctionne sur des chaînes de caractères. Si vous avez besoin de stocker un int ou un bool, vous devez les convertir en String :
// Convertir en String avant de sauvegarder
await storage.write(key: 'age', value: age.toString());
await storage.write(key: 'isPremium', value: isPremium.toString());
// Convertir en int/bool lors de la lecture
int age = int.parse(await storage.read(key: 'age') ?? '0');
bool isPremium = (await storage.read(key: 'isPremium') ?? 'false') == 'true';
⚡ Performance
SharedPreferences est généralement plus rapide car :
- Pas de chiffrement/déchiffrement
- Accès direct aux fichiers
- Moins de surcharge
Secure Storage est légèrement plus lent car :
- Chiffrement avant écriture
- Déchiffrement après lecture
- Accès au KeyStore/Keychain
La différence de performance est généralement négligeable pour la plupart des applications. La sécurité prime sur la performance pour les données sensibles.
📋 Tableau comparatif complet
Légende : ✓✓✓ Excellent | ✓✓ Très bon | ✓ Bon | ✗ Faible
🤔 Quand utiliser chaque solution ?
✅ Utilisez SharedPreferences pour :
- Thème (clair/sombre)
- Langue de l'application
- Volume sonore
- Paramètres d'affichage
- Compteurs et statistiques
- Toute donnée non sensible
✅ Utilisez Secure Storage pour :
- Tokens d'authentification
- Mots de passe
- API keys
- Clés de chiffrement
- Secrets d'application
- Toute donnée sensible
Utilisez les deux solutions dans la même application ! SharedPreferences pour les préférences utilisateur, et Secure Storage pour les données sensibles. C'est la meilleure approche.
5.2.6 – Bonnes pratiques de sécurité
Utiliser Secure Storage est un bon début, mais il existe d'autres bonnes pratiques de sécurité à suivre pour protéger efficacement les données de vos utilisateurs.
🔒 Règles fondamentales
1. Ne jamais stocker de mots de passe en clair
Même avec Secure Storage, évitez de stocker les mots de passe directement. Utilisez plutôt des tokens d'authentification qui peuvent être révoqués.
Si vous devez absolument stocker un mot de passe, utilisez Secure Storage. Mais idéalement, utilisez un système d'authentification qui génère des tokens au lieu de stocker les mots de passe.
2. Utiliser des noms de clés descriptifs mais non évidents
Utilisez des noms de clés clairs dans votre code, mais évitez des noms trop évidents qui pourraient révéler la nature des données :
// ✅ Bon : Nom descriptif mais pas trop évident
await storage.write(key: 'auth_token', value: token);
await storage.write(key: 'refresh_token', value: refreshToken);
// ❌ Éviter : Trop évident
await storage.write(key: 'password_123', value: password);
await storage.write(key: 'credit_card_number', value: cardNumber);
3. Valider les données avant de les stocker
Toujours valider les données avant de les sauvegarder :
Future<void> sauvegarderToken(String token) async {
// Valider le token avant de le sauvegarder
if (token.isEmpty) {
throw Exception('Le token ne peut pas être vide');
}
if (token.length < 10) {
throw Exception('Le token semble invalide');
}
// Sauvegarder seulement si valide
await storage.write(key: 'auth_token', value: token);
}
4. Gérer les erreurs de manière appropriée
Utilisez try-catch pour gérer les erreurs potentielles :
Future<void> sauvegarderToken(String token) async {
try {
await storage.write(key: 'auth_token', value: token);
} catch (e) {
print('Erreur lors de la sauvegarde : $e');
// Gérer l'erreur (afficher un message, logger, etc.)
}
}
🛡️ Protection supplémentaire
1. Expiration des tokens
Stockez la date d'expiration avec le token et vérifiez-la avant utilisation :
// Sauvegarder le token avec sa date d'expiration
await storage.write(key: 'auth_token', value: token);
await storage.write(
key: 'token_expires_at',
value: DateTime.now().add(Duration(hours: 24)).toIso8601String(),
);
// Vérifier l'expiration avant utilisation
String? token = await storage.read(key: 'auth_token');
String? expiresAt = await storage.read(key: 'token_expires_at');
if (token != null && expiresAt != null) {
DateTime expiration = DateTime.parse(expiresAt);
if (DateTime.now().isAfter(expiration)) {
// Token expiré, le supprimer
await storage.delete(key: 'auth_token');
await storage.delete(key: 'token_expires_at');
token = null;
}
}
2. Nettoyer les données à la déconnexion
Supprimez toutes les données sensibles quand l'utilisateur se déconnecte :
Future<void> deconnexion() async {
final storage = FlutterSecureStorage();
// Supprimer toutes les données sensibles
await storage.deleteAll();
// Optionnel : Supprimer aussi les préférences non sensibles
final prefs = await SharedPreferences.getInstance();
await prefs.remove('username'); // Si stocké dans SharedPreferences
}
3. Ne pas logger les données sensibles
Évitez de logger ou d'afficher les données sensibles dans la console :
// ❌ Ne pas faire
String token = await storage.read(key: 'auth_token');
print('Token : $token'); // DANGEREUX !
// ✅ Faire plutôt
String? token = await storage.read(key: 'auth_token');
if (token != null) {
print('Token présent (${token.length} caractères)');
} else {
print('Aucun token trouvé');
}
📋 Checklist de sécurité
- ✅ Tous les mots de passe et tokens utilisent Secure Storage
- ✅ Les données sensibles ne sont jamais loggées
- ✅ Les tokens ont une expiration
- ✅ Les données sont nettoyées à la déconnexion
- ✅ Les erreurs sont gérées proprement
- ✅ Les données sont validées avant stockage
🔐 Exemple complet : Gestion sécurisée d'authentification
Voici un exemple complet qui montre les bonnes pratiques :
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class AuthService {
final _storage = FlutterSecureStorage();
// Sauvegarder un token avec expiration
Future<void> sauvegarderToken(String token) async {
try {
// Valider le token
if (token.isEmpty || token.length < 10) {
throw Exception('Token invalide');
}
// Sauvegarder le token
await _storage.write(key: 'auth_token', value: token);
// Sauvegarder la date d'expiration (24 heures)
final expiration = DateTime.now().add(Duration(hours: 24));
await _storage.write(
key: 'token_expires_at',
value: expiration.toIso8601String(),
);
} catch (e) {
print('Erreur lors de la sauvegarde du token : $e');
rethrow;
}
}
// Récupérer le token (vérifie l'expiration)
Future<String?> getToken() async {
try {
String? token = await _storage.read(key: 'auth_token');
String? expiresAt = await _storage.read(key: 'token_expires_at');
if (token == null) {
return null;
}
// Vérifier l'expiration
if (expiresAt != null) {
DateTime expiration = DateTime.parse(expiresAt);
if (DateTime.now().isAfter(expiration)) {
// Token expiré, le supprimer
await deconnexion();
return null;
}
}
return token;
} catch (e) {
print('Erreur lors de la lecture du token : $e');
return null;
}
}
// Déconnexion : supprimer toutes les données
Future<void> deconnexion() async {
try {
await _storage.deleteAll();
} catch (e) {
print('Erreur lors de la déconnexion : $e');
}
}
// Vérifier si l'utilisateur est connecté
Future<bool> estConnecte() async {
String? token = await getToken();
return token != null;
}
}
Cet exemple montre :
- Validation des données avant stockage
- Gestion des erreurs avec try-catch
- Expiration des tokens
- Nettoyage à la déconnexion
- Pas de logging des données sensibles
La sécurité est un processus continu. Restez informé des meilleures pratiques et mettez régulièrement à jour vos dépendances pour bénéficier des dernières corrections de sécurité.
Maintenant que vous maîtrisez Secure Storage, mettez vos connaissances en pratique avec l'exercice ci-dessous ! Vous allez créer une application d'authentification complète avec persistance sécurisée des identifiants.
🎯 Exercice pratique
Objectif : Créer une application d'authentification avec une interface élégante. L'application doit permettre de se connecter avec un nom d'utilisateur et un mot de passe. Les identifiants doivent être sauvegardés de manière sécurisée avec Secure Storage. Si l'utilisateur est déjà authentifié, il doit être redirigé automatiquement vers la page d'accueil au démarrage de l'application.
📝 Instructions :
- Identifiez les défis : Observez les fonctionnalités requises et notez les défis techniques (par exemple : "Comment vérifier si l'utilisateur est déjà connecté au démarrage ?", "Comment sauvegarder le mot de passe de manière sécurisée ?", "Comment naviguer vers la page d'accueil après connexion ?", etc.)
- Notez vos solutions : Avant de regarder le code, essayez de noter comment vous résoudriez chaque défi avec Secure Storage (vérification dans initState(), sauvegarde après connexion, navigation conditionnelle, etc.)
- Comparez avec les solutions : Cliquez sur "Afficher le code" ci-dessous pour voir les solutions proposées et comparer avec vos notes. Analysez comment l'authentification et la persistance sont gérées.
🎯 Fonctionnalités à implémenter :
- Interface de connexion avec TextField pour username et password (texte masqué)
- Sauvegarde sécurisée du username et password avec Secure Storage
- Vérification au démarrage : si les identifiants sont sauvegardés, redirection automatique vers la page d'accueil
- Page d'accueil simple avec un message de bienvenue et un bouton de déconnexion
- Déconnexion : supprimer les identifiants et retourner à l'écran de connexion
- Gestion des erreurs (champs vides, identifiants incorrects)
- Vérification au démarrage :
_verifierAuthentification()est appelé dansinitState()pour vérifier si l'utilisateur est déjà connecté - Navigation conditionnelle : Si les identifiants existent, redirection automatique vers la page d'accueil avec
pushReplacement - Sauvegarde sécurisée : Les identifiants sont sauvegardés avec Secure Storage après une connexion réussie
- Déconnexion : Le bouton de déconnexion supprime les identifiants et retourne à l'écran de connexion
- Validation : Utilisation de
FormetTextFormFieldpour valider les champs - Interface élégante : Design moderne avec Material Design 3, icônes, et espacements harmonieux