Se connecter à Flutter avec Spring Boot
Sommaire
- 1- Objectif
- 2- Présentation
- 3- Configuration du projet Spring Boot
- 3.1- Étape 1 : Créer la base de données MySQL
- 3.2- Étape 2 : Créer le projet Spring Boot
- 3.3- Étape 3 : Configuration de la base de données
- 3.4- Étape 4 : Créer l'entité Utilisateur
- 3.5- Étape 5 : Créer le Repository
- 3.6- Étape 6 : Configuration de sécurité
- 3.7- Étape 7 : Créer le contrôleur d'authentification
- 3.8- Étape 8 : Créer un script pour insérer un utilisateur de test
- 4- Créer le projet Flutter
- 4.1- Étape 1 : Créer le projet Flutter
- 4.2- Étape 2 : Créer un service d'authentification
- 4.3- Étape 3 : Créer l'interface de connexion Flutter
- 4.4- Étape 4 : Créer un modèle Utilisateur pour Flutter
- 5- Exercices
- 5.1.1- Cours Flutter
Se connecter à Flutter avec Spring Boot
-
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.
-
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
: -
É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 :
-
Étape 4 : Créer l’entité Utilisateur
-
Étape 5 : Créer le Repository
-
Étape 6 : Configuration de sécurité
-
Étape 7 : Créer le contrôleur d’authentification
-
Étape 8 : Créer un script pour insérer un utilisateur de test
-
Créer le projet Flutter
-
Étape 1 : Créer le projet Flutter
- Ajouter la dépendance HTTP dans
pubspec.yaml
: -
Étape 2 : Créer un service d’authentification
- Fichier : auth_service.dart
-
Étape 3 : Créer l’interface de connexion Flutter
- Fichier :
main.dart
- Fichier :
login_page.dart
-
Étape 4 : Créer un modèle Utilisateur pour Flutter
-
Exercices
- 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
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;
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
@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 List roles = 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
}
@Repository
public interface UtilisateurRepository extends JpaRepository {
Optional findByEmail(String email);
Boolean existsByEmail(String email);
}
@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();
}
}
@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 {
Optional utilisateurOpt = 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
}
}
@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");
}
}
}
flutter create flutter_login_app
dependencies:
flutter:
sdk: flutter
http: ^0.13.5
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
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(),
);
}
}
import '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
State createState() => _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)),
),
],
),
),
);
}
}
class Utilisateur {
final int id;
final String email;
final List roles;
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,
};
}