Bangun sistem login profesional seperti aplikasi nyata
Tekan → atau klik NEXT untuk melanjutkan
class AuthService {
Future<Map<String, dynamic>> login({
required String email,
required String password,
}) async {
final response = await http.post(
Uri.parse('https://api.example.com/auth/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'email': email,
'password': password,
}),
);
if (response.statusCode == 200) {
// ✅ Login berhasil!
return jsonDecode(response.body);
} else if (response.statusCode == 401) {
// ❌ Email atau password salah
throw Exception('Kredensial tidak valid');
} else {
throw Exception('Error: ${response.statusCode}');
}
}
}
Future<Map<String, dynamic>> register({
required String name,
required String email,
required String password,
}) async {
// Validasi sebelum kirim ke server
if (password.length < 8) {
throw Exception('Password minimal 8 karakter');
}
final response = await http.post(
Uri.parse('https://api.example.com/auth/register'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'name': name,
'email': email,
'password': password,
}),
);
if (response.statusCode == 201) {
// ✅ Akun berhasil dibuat!
return jsonDecode(response.body);
} else if (response.statusCode == 409) {
throw Exception('Email sudah terdaftar');
} else {
throw Exception('Registrasi gagal');
}
}
Future<void> logout(String accessToken) async {
try {
// Langkah 1: Beri tahu server untuk invalidate token
final response = await http.post(
Uri.parse('https://api.example.com/auth/logout'),
headers: {
'Authorization': 'Bearer $accessToken',
'Content-Type': 'application/json',
},
);
if (response.statusCode != 200) {
// Jika server response error, tetap lanjut ke langkah 2
debugPrint('Server logout gagal');
}
} catch (e) {
// Jika request gagal (network error), tetap lanjut
debugPrint('Logout error: $e');
} finally {
// Langkah 2: SELALU hapus token lokal, apapun hasilnya
await _storage.clearAll();
// Langkah 3: Redirect ke login
_navigateToLogin();
}
}
finally dijalankan SELALU, baik try berhasil atau catch. Ini memastikan token lokal selalu dihapus, jadi user TIDAK bisa masuk tanpa login ulang!
HEADER.PAYLOAD.SIGNATURE
// Contoh JWT asli dari server:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiJ1c2VyXzEyMyIsIm5hbWUiOiJCdWRpIFNhbnRvc28iLCJlbWFpbCI6ImJ1ZGlAZW1haWwuY29tIiwicm9sZSI6InN0dWRlbnQiLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
// Struktur:
[Base64(Header)] . [Base64(Payload)] . [HmacSha256(Secret)]
| Bagian | Bisa Dibaca? | Bisa Diubah? | Fungsi |
|---|---|---|---|
| HEADER | ✅ Ya (Base64) | ❌ Tidak | Tahu algoritma & tipe |
| PAYLOAD | ✅ Ya (Base64) | ❌ Tidak | Data user (public info) |
| SIGNATURE | ✅ Ya (Base64) | ❌ Tidak* | Validasi token asli dari server |
{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "user_123",
"name": "Budi",
"email": "budi@email.com",
"role": "student",
"iat": 1700000000,
"exp": 1700003600
}
HMACSHA256(
base64url(header) +
"." +
base64url(payload),
"SECRET_KEY"
)
Authorization: Bearer eyJhbGc...
// Kirim access token di setiap request
final response = await http.get(
Uri.parse('https://api.example.com/user/profile'),
headers: {
'Authorization': 'Bearer $accessToken',
},
);
if (response.statusCode == 401) {
// Token expired! Perlu refresh
await refreshAccessToken();
}
// FLOW: Access token expired → gunakan refresh token
if (response.statusCode == 401) {
// Access token expired! Gunakan refresh token
final refreshToken = await _storage.getRefreshToken();
final newResponse = await http.post(
Uri.parse('$_baseUrl/auth/refresh'),
body: jsonEncode({'refresh_token': refreshToken}),
);
if (newResponse.statusCode == 200) {
// ✅ Dapat token baru!
final data = jsonDecode(newResponse.body);
await _storage.saveAccessToken(data['access_token']);
// Retry request asli
return await _retryRequest(originalRequest);
} else {
// ❌ Refresh gagal = harus login ulang
await _logout();
}
}
HttpClient wrapper yang otomatis handle refresh token saat 401. Jadi di setiap screen, dev hanya perlu panggil API biasa saja!
| Fitur | SharedPreferences | Secure Storage |
|---|---|---|
| Enkripsi | ❌ Plain text | ✅ AES-256 |
| OS Protection | ❌ Visible | ✅ Keychain (iOS) / Keystore (Android) |
| Root/Jailbreak | ❌ Data bisa diambil | ✅ Protected bahkan dengan root |
| Cocok untuk | Theme, Language, Preference UI | 🔑 Token, Password, API Key, Secret |
| Kecepatan | Sangat cepat | Cepat (tapi sedikit lebih lambat) |
flutter_secure_storage: ^9.0.0
// ✅ BENAR: Simpan token dengan Secure Storage
final storage = FlutterSecureStorage();
// Simpan token
await storage.write(
key: 'access_token',
value: token,
);
// Baca token
final token = await storage.read(
key: 'access_token',
);
// Hapus token
await storage.delete(key: 'access_token');
// Hapus SEMUA
await storage.deleteAll();
class SessionManager {
final _storage = FlutterSecureStorage();
// 1. Apakah user punya sesi aktif?
Future<bool> hasActiveSession() async {
final token = await _storage.read(
key: 'refresh_token',
);
return token != null && token.isNotEmpty;
}
// 2. Mulai sesi setelah login berhasil
Future<void> startSession({
required String accessToken,
required String refreshToken,
}) async {
await _storage.write(
key: 'access_token', value: accessToken);
await _storage.write(
key: 'refresh_token', value: refreshToken);
await _storage.write(
key: 'login_time',
value: DateTime.now().toIso8601String());
}
// 3. Akhiri sesi saat logout
Future<void> endSession() async {
await _storage.deleteAll();
}
}
class SplashScreen extends StatefulWidget {
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
_checkAutoLogin(); // ← Panggil saat widget dibuat
}
Future<void> _checkAutoLogin() async {
// Tampilkan splash 2 detik
await Future.delayed(
const Duration(seconds: 2),
);
// Cek: apakah ada sesi aktif?
final hasSession = await SessionManager()
.hasActiveSession();
if (!mounted) return;
// Navigasi berdasarkan hasil
Navigator.pushReplacementNamed(
context,
hasSession ? '/home' : '/login',
);
}
}
// JENIS 1: Token Expired (401 dari server)
if (response.statusCode == 401) {
// Token expired atau invalid!
await _logout();
// Redirect ke login dengan pesan
_showSnackBar('Sesi habis, silakan login kembali');
}
// JENIS 2: Inactivity Timeout (30 menit tidak aktif)
class InactivityTimer {
static Timer? _timer;
static const _timeout = Duration(minutes: 30);
// Panggil ini setiap user input (tap, scroll, ketik)
static void reset(BuildContext context) {
_timer?.cancel();
_timer = Timer(_timeout, () {
// 30 menit berlalu tanpa input!
_logout(context);
});
}
static Future<void> _logout(
BuildContext context) async {
await SessionManager().endSession();
Navigator.pushNamedAndRemoveUntil(
context, '/login', (_) => false,
);
}
}
GestureDetector(
onTap: () => InactivityTimer.reset(context),
child: YourWidget(),
)
# pubspec.yaml
name: authentication_app
description: Aplikasi autentikasi mobile profesional
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
http: ^1.2.0
flutter_secure_storage: ^9.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
uses-material-design: true
flutter create authentication_appflutter pub getclass _LoginScreenState extends State<LoginScreen> {
final _emailCtrl = TextEditingController();
final _passwordCtrl = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
@override
void dispose() {
_emailCtrl.dispose();
_passwordCtrl.dispose();
super.dispose();
}
Future<void> _handleLogin() async {
// 1️⃣ Validasi form
if (!_formKey.currentState!.validate()) {
return;
}
setState(() => _isLoading = true);
try {
// 2️⃣ Panggil auth service
final result = await AuthService().login(
email: _emailCtrl.text.trim(),
password: _passwordCtrl.text,
);
// 3️⃣ Simpan token ke secure storage
await SessionManager().startSession(
accessToken: result['access_token'],
refreshToken: result['refresh_token'],
);
if (!mounted) return;
// 4️⃣ Navigasi ke HOME
Navigator.pushReplacementNamed(context, '/home');
} catch (e) {
// Tampilkan error dengan snackbar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Login gagal: $e'),
backgroundColor: Colors.red,
),
);
} finally {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailCtrl,
decoration: InputDecoration(
labelText: 'Email',
),
validator: (val) {
if (val?.isEmpty ?? true) {
return 'Email tidak boleh kosong';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
controller: _passwordCtrl,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
),
validator: (val) {
if (val?.length ?? 0 < 8) {
return 'Password min 8 karakter';
}
return null;
},
),
SizedBox(height: 24),
_isLoading
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: _handleLogin,
child: const Text('Login'),
),
],
),
),
);
}
}
class _RegisterScreenState extends State<RegisterScreen> {
final _nameCtrl = TextEditingController();
final _emailCtrl = TextEditingController();
final _passwordCtrl = TextEditingController();
final _confirmCtrl = TextEditingController();
bool _isLoading = false;
Future<void> _handleRegister() async {
// Validasi email format
final emailRegex = RegExp(
r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+',
);
if (!emailRegex.hasMatch(_emailCtrl.text)) {
_showError('Email format tidak valid');
return;
}
// Validasi password match
if (_passwordCtrl.text != _confirmCtrl.text) {
_showError('Password tidak cocok');
return;
}
setState(() => _isLoading = true);
try {
await AuthService().register(
name: _nameCtrl.text,
email: _emailCtrl.text.trim(),
password: _passwordCtrl.text,
);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Akun berhasil dibuat! Silakan login.'),
backgroundColor: Colors.green,
),
);
Navigator.pushReplacementNamed(
context, '/login',
);
} catch (e) {
_showError('Register gagal: $e');
} finally {
setState(() => _isLoading = false);
}
}
void _showError(String msg) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(msg)),
);
}
}
class _HomeScreenState extends State<HomeScreen> {
final _session = SessionManager();
String? _userName;
bool _isLoggingOut = false;
@override
void initState() {
super.initState();
_loadUserData();
}
Future<void> _loadUserData() async {
// Ambil data user dari storage atau API
final name = await _session.getCurrentUserName();
setState(() => _userName = name);
}
Future<void> _handleLogout() async {
setState(() => _isLoggingOut = true);
try {
// 1️⃣ Ambil token
final token = await FlutterSecureStorage()
.read(key: 'access_token');
// 2️⃣ Kirim logout ke server
if (token != null) {
await AuthService().logout(token);
}
// 3️⃣ Hapus token lokal
await _session.endSession();
if (!mounted) return;
// 4️⃣ Navigate ke login
Navigator.pushReplacementNamed(
context, '/login',
);
} catch (e) {
// Even if error, still force logout local
await _session.endSession();
} finally {
setState(() => _isLoggingOut = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
IconButton(
onPressed: _handleLogout,
icon: const Icon(Icons.logout),
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.account_circle,
size: 80,
),
Text(
'Selamat datang, $_userName!',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
_isLoggingOut
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: _handleLogout,
child: const Text('Logout'),
),
],
),
),
);
}
}
https://reqres.in/api (API publik gratis untuk testing)
auth_tugas_[nama_kamu]/