Objectif
- À la fin de ce tutoriel, vous serez capable de :
- Créer une base de données MySQL simple pour l’authentification.
- Écrire un backend Spring Boot pour la connexion à MySQL.
- Échanger des données entre Flutter et Spring Boot.
- Créer une interface de connexion Flutter avec vérification côté serveur.
- À la fin de ce tutoriel, vous serez capable de :
Présentation
- Ce tutoriel explique comment connecter une application Flutter à une base de données MySQL à l'aide d'un backend Spring Boot. Flutter ne peut pas accéder directement à MySQL, d'où l'importance de l'API Spring Boot.
Configuration du projet Spring Boot
Étape 1 : Créer la base de données MySQL
- Créer la base de données :
- Créer la table
utilisateurs:
CREATE DATABASE flutter_app;CREATE TABLE `utilisateurs` ( `id` int NOT NULL AUTO_INCREMENT, `email` varchar(180) NOT NULL, `roles` json NOT NULL, `password` varchar(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `UNIQ_EMAIL` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;Étape 2 : Créer le projet Spring Boot
- Utilisez Spring Initializr (https://start.spring.io/) avec les dépendances :
- Configure le projet :
- Project : Maven (recommandé)
- Language : Java
- Spring Boot : laisse la dernière version stable
- Project Metadata :
- Group : com.example
- Artifact : myapi
- Packaging : Jar
- Java : 17 (ou 21 si tu as installé la version récente)
- Ajouter les dépendances :
- Spring Web
- Spring Data JPA
- MySQL Driver
- (optionnel) Spring Security si tu veux gérer l’authentification
- Clique sur
Generate→ ça télécharge un fichier .zip - Décompresse et ouvre le projet dans IntelliJ IDEA ou Eclipse
Étape 3 : Configuration de la base de données
- Fichier application.properties:
spring.datasource.url=jdbc:mysql://localhost:3306/flutter_app spring.datasource.username=root spring.datasource.password= spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect spring.jpa.show-sql=true # CORS configuration spring.web.cors.allowed-origins=http://localhost:3000,http://10.0.2.2 spring.web.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS spring.web.cors.allowed-headers=* spring.web.cors.allow-credentials=true Étape 4 : Créer l'entité Utilisateur
@Entity @Table(name = "utilisateurs") public class Utilisateur { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String email; @Column(nullable = false) private String password; @ElementCollection(fetch = FetchType.EAGER) private Listroles = new ArrayList<>(); // Constructeurs, getters et setters public Utilisateur() {} public Utilisateur(String email, String password, List roles) { this.email = email; this.password = password; this.roles = roles; } // Getters et setters pour tous les champs } Étape 5 : Créer le Repository
@Repository public interface UtilisateurRepository extends JpaRepository{ Optional findByEmail(String email); Boolean existsByEmail(String email); } Étape 6 : Configuration de sécurité
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors().and() .csrf().disable() .authorizeHttpRequests(authz -> authz .requestMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ); return http.build(); } }Étape 7 : Créer le contrôleur d'authentification
@RestController @RequestMapping("/api/auth") @CrossOrigin(origins = "*") public class AuthController { @Autowired private UtilisateurRepository utilisateurRepository; @Autowired private PasswordEncoder passwordEncoder; @PostMapping("/login") public ResponseEntity> login(@RequestBody LoginRequest loginRequest) { try { OptionalutilisateurOpt = utilisateurRepository.findByEmail(loginRequest.getEmail()); if (utilisateurOpt.isEmpty()) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(new AuthResponse(false, "Identifiants invalides", null)); } Utilisateur utilisateur = utilisateurOpt.get(); if (!passwordEncoder.matches(loginRequest.getPassword(), utilisateur.getPassword())) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(new AuthResponse(false, "Mot de passe incorrect", null)); } // Créer une réponse avec les informations utilisateur UserResponse userResponse = new UserResponse( utilisateur.getId(), utilisateur.getEmail(), utilisateur.getRoles() ); return ResponseEntity.ok(new AuthResponse(true, "Connexion réussie", userResponse)); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new AuthResponse(false, "Erreur serveur: " + e.getMessage(), null)); } } // Classes internes pour les requêtes et réponses public static class LoginRequest { private String email; private String password; // Getters et setters public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } public static class AuthResponse { private boolean success; private String message; private UserResponse user; // Constructeurs, getters et setters public AuthResponse(boolean success, String message, UserResponse user) { this.success = success; this.message = message; this.user = user; } // Getters et setters } public static class UserResponse { private Long id; private String email; private List roles; // Constructeurs, getters et setters public UserResponse(Long id, String email, List roles) { this.id = id; this.email = email; this.roles = roles; } // Getters et setters } } Étape 8 : Créer un script pour insérer un utilisateur de test
@Component public class DataLoader implements CommandLineRunner { @Autowired private UtilisateurRepository utilisateurRepository; @Autowired private PasswordEncoder passwordEncoder; @Override public void run(String... args) throws Exception { // Vérifier si l'utilisateur test existe déjà if (utilisateurRepository.findByEmail("nodejs@example.com").isEmpty()) { Utilisateur utilisateur = new Utilisateur(); utilisateur.setEmail("nodejs@example.com"); utilisateur.setPassword(passwordEncoder.encode("123456")); utilisateur.setRoles(Arrays.asList("ROLE_USER")); utilisateurRepository.save(utilisateur); System.out.println("Utilisateur test créé avec succès"); } } }
- Fichier application.properties:
Créer le projet Flutter
Étape 1 : Créer le projet Flutter
flutter create flutter_login_app- Ajouter la dépendance HTTP dans
pubspec.yaml: Étape 2 : Créer un service d'authentification
- Fichier : auth_service.dart
import 'dart:convert'; import 'package:http/http.dart' as http; class AuthService { static const String baseUrl = "http://10.0.2.2:8080/api/auth"; static FutureÉtape 3 : Créer l'interface de connexion Flutter
- Fichier :
main.dart
import 'package:flutter/material.dart'; import 'login_page.dart'; void main() { runApp(const MainApp()); } class MainApp extends StatelessWidget { const MainApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( debugShowCheckedModeBanner: false, home: LoginPage(), ); } }- Fichier :
- Fichier :
login_page.dartimport 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'home_page.dart'; import 'auth_service.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override StatecreateState() => _LoginPageState(); } class _LoginPageState extends State { final _emailCtrl = TextEditingController(); final _passwordCtrl = TextEditingController(); String errorMsg = ''; bool isLoading = false; Future _login() async { setState(() { isLoading = true; errorMsg = ''; }); try { final response = await AuthService.login( _emailCtrl.text.trim(), _passwordCtrl.text.trim(), ); if (response['success'] == true) { Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const HomePage()), ); } else { setState(() { errorMsg = response['message'] ?? 'Identifiants incorrects'; }); } } catch (e) { setState(() { errorMsg = 'Erreur de connexion au serveur: $e'; }); } finally { setState(() { isLoading = false; }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Login")), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ TextField( controller: _emailCtrl, decoration: const InputDecoration(labelText: "Email"), keyboardType: TextInputType.emailAddress, ), TextField( controller: _passwordCtrl, decoration: const InputDecoration(labelText: "Password"), obscureText: true, ), const SizedBox(height: 16), isLoading ? const CircularProgressIndicator() : ElevatedButton( onPressed: _login, child: const Text("Login"), ), if (errorMsg.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 12.0), child: Text(errorMsg, style: const TextStyle(color: Colors.red)), ), ], ), ), ); } } Étape 4 : Créer un modèle Utilisateur pour Flutter
class Utilisateur { final int id; final String email; final Listroles; Utilisateur({ required this.id, required this.email, required this.roles, }); factory Utilisateur.fromJson(Map json) { return Utilisateur( id: json['id'], email: json['email'], roles: List .from(json['roles']), ); } Map toJson() => { 'id': id, 'email': email, 'roles': roles, }; }
dependencies: flutter: sdk: flutter http: ^0.13.5Exercices
- Exercice 1 : Ajoutez une fonctionnalité d'inscription avec hachage de mot de passe côté Spring Boot.
- Exercice 2 : Implémentez JWT (JSON Web Tokens) pour une authentification stateless
- Exercice 3 : Créez une page de profil affichant l'email après connexion
- Exercice 4 : Ajoutez la gestion des erreurs et les messages de validation