Cara Menambahkan Autentikasi Login ke Aplikasi React
Penulis memilih Creative Commons untuk menerima donasi sebagai bagian dari program Write for Donations.
Pendahuluan
Autentikasi dalam aplikasi React mengelola akses pengguna ke halaman dan sumber daya yang dilindungi. Ketika seorang pengguna masuk, aplikasi Anda menyimpan kredensial autentikasi (biasanya berupa token) dan memeriksanya sebelum menampilkan rute pribadi. Status autentikasi harus bertahan saat halaman di-refresh, yang membutuhkan penyimpanan token di penyimpanan browser daripada di state komponen.
Bagaimana status autentikasi bertahan: Ketika Anda menyimpan token di localStorage atau sessionStorage, token tersebut tetap ada saat halaman diperbarui dan browser di-restart (untuk localStorage). Ini berarti aplikasi Anda dapat memeriksa token yang sudah ada saat dimuat dan secara otomatis mengautentikasi pengguna tanpa memerlukan login baru. Tanpa penyimpanan yang persisten, pengguna harus login setiap kali mereka menyegarkan halaman atau membuka tab baru.
Tutorial ini menjelaskan cara membangun aplikasi React dengan otentikasi berbasis token menggunakan React Router v6 untuk rute yang dilindungi. Anda akan membuat API tiruan yang mengembalikan token otentikasi, mengimplementasikan halaman login yang mengambil dan menyimpan token, serta mengatur pemeriksaan otentikasi yang menjaga rute yang diinginkan pengguna. Tutorial ini mencakup berbagai metode penyimpanan token (localStorage, sessionStorage) dan implikasi keamanannya, mengimplementasikan fungsi logout dan penanganan kesalahan, serta menunjukkan cara mendekode token JWT untuk memeriksa masa berlakunya.
Mengapa logika otentikasi sebaiknya tidak ditempatkan di komponen individual: Menyentralisasi logika otentikasi dalam custom hook atau React Context memastikan perilaku yang konsisten di seluruh aplikasi Anda, memudahkan pembaruan alur otentikasi, dan mencegah duplikasi kode. Ketika logika otentikasi tersebar di berbagai komponen, Anda berisiko menghadapi pemeriksaan token yang tidak konsisten, kasus-kasus tepi yang terlewat, dan kesulitan dalam pemeliharaan.
Setelah menyelesaikan tutorial ini, Anda akan memahami cara menerapkan autentikasi yang aman dalam aplikasi React, memilih strategi penyimpanan token yang tepat berdasarkan kebutuhan keamanan dan pengalaman pengguna, serta menangani skenario autentikasi umum termasuk rute yang dilindungi, kedaluwarsa token, dan logout.
Perlu men-deploy proyek React dengan cepat? Coba Linux-Console.net App Platform dan deploy proyek React langsung dari GitHub dalam hitungan menit.
Poin Penting
Status autentikasi tetap ada meskipun halaman di-refresh saat disimpan di
localStorageatausessionStorage. Token yang disimpan di state komponen (useState) akan hilang saat halaman di-refresh, sehingga pengguna harus login lagi. API penyimpanan browser mempertahankan token antar sesi, memungkinkan autentikasi otomatis.React Router v6 menggunakan
Routesalih-alihSwitchdan memerlukan propertielementalih-alih children. API baru ini lebih deklaratif dan aman terhadap tipe. Rute yang dilindungi diimplementasikan menggunakan komponen pembungkus yang memeriksa autentikasi sebelum merender.Sentralisasikan logika autentikasi dalam custom hook atau React Context. Menyimpan logika auth di komponen individual menyebabkan duplikasi dan perilaku yang tidak konsisten. Sebuah custom
useTokenhook atauAuthContextmenyediakan satu sumber kebenaran untuk status autentikasi.localStoragebertahan di seluruh sesi browser;sessionStoragespesifik untuk tab. GunakanlocalStorageuntuk fungsionalitas “ingat saya” dansessionStorageuntuk autentikasi yang lebih aman hanya selama sesi. Keduanya rentan terhadap serangan XSS, jadi selalu bersihkan input pengguna.Token JWT dapat di-decode di sisi klien untuk memeriksa masa berlaku tanpa perlu bolak-balik ke server. Payload berisi klaim
exp(timestamp Unix) yang dapat Anda bandingkan dengan waktu saat ini. Namun, selalu validasi token di server karena pemeriksaan di sisi klien hanya untuk pengalaman pengguna (UX) saja.Perlindungan CSRF sangat penting saat menggunakan autentikasi berbasis cookie. Cookie dikirimkan secara otomatis dengan setiap permintaan, sehingga rentan terhadap pemalsuan permintaan lintas situs. Gunakan token CSRF, atribut cookie SameSite, dan verifikasi header Origin di server.
Prasyarat
Lingkungan pengembangan yang menjalankan Node.js versi 18.x dan npm versi 9.x. Untuk menginstal Node.js di macOS atau Ubuntu, ikuti langkah-langkah di Cara Menginstal Node.js dan Membuat Lingkungan Pengembangan Lokal di macOS atau Cara Menginstal Node.js di Ubuntu.
-
Lingkungan pengembangan React yang disiapkan dengan Create React App, dengan boilerplate yang tidak penting dihapus. Untuk mengatur ini, ikuti Langkah 1 — Membuat Proyek Kosong dari tutorial Cara Mengelola State pada Komponen Kelas React. Tutorial ini menggunakan
auth-tutorialsebagai nama proyek. Familiar dengan pengambilan data dari API di React. Untuk informasi lebih lanjut tentang bekerja dengan API, lihat Cara Memanggil Web API dengan Hook useEffect di React.
Pengetahuan dasar tentang JavaScript, HTML, dan CSS. Pelajari lebih lanjut dalam seri Cara Membuat Situs Web dengan HTML, Cara Menata HTML dengan CSS, dan Cara Pemrograman dengan JavaScript.
Langkah 1 — Membuat Halaman Login
Halaman login mencegat pengguna yang belum terautentikasi sebelum mereka mencapai rute yang dilindungi. Alih-alih mengarahkan pengguna ke rute login khusus (yang membuat mereka kehilangan tujuan awal), tampilkan formulir login di rute manapun ketika tidak ada token autentikasi. Ini mempertahankan tujuan navigasi pengguna untuk mengunjungi /dashboard saat keluar yang menampilkan formulir login, dan setelah autentikasi, mereka akan langsung menuju /dashboard alih-alih diarahkan ke tempat lain.
Mengapa menampilkan login di rute mana pun: Ketika pengguna mengunjungi rute yang terlindungi seperti /dashboard sambil tidak terautentikasi, menampilkan formulir login di rute yang sama mempertahankan konteks navigasi mereka. Setelah login, mereka sudah berada di tujuan yang dimaksud tanpa perlu pengalihan tambahan. Ini menciptakan pengalaman pengguna yang lebih lancar dibandingkan mengalihkan ke /login dan kemudian memerlukan langkah navigasi tambahan.
Pasang React Router menggunakan npm. Ada dua versi: versi web untuk aplikasi React dan versi native untuk React Native. Pasang versi web:
npm install react-router-domInstalasi selesai dengan output yang mirip dengan:
...
+ react-router-dom@6.28.0
added 11 packages from 6 contributors, removed 10 packages and audited 1945 packages in 12.794s
...Selanjutnya, buat dua komponen bernama Dashboard dan Preferences yang berfungsi sebagai halaman pribadi. Ini akan merepresentasikan komponen yang tidak boleh dilihat pengguna sampai mereka berhasil masuk ke aplikasi.
Pertama, buat direktori:
mkdir src/components/Dashboard
mkdir src/components/PreferencesBuka Dashboard.js di editor teks:
nano src/components/Dashboard/Dashboard.js
Di dalam Dashboard.js, tambahkan tag <h2> dengan konten Dashboard:
import React from 'react';
export default function Dashboard() {
return(
<h2>Dashboard</h2>
);
}Simpan dan tutup berkas.
Ulangi langkah yang sama untuk Preferences. Buka komponen:
nano src/components/Preferences/Preferences.jsTambahkan kontennya:
import React from 'react';
export default function Preferences() {
return(
<h2>Preferences</h2>
);
}Simpan dan tutup berkas.
Sekarang setelah Anda memiliki beberapa komponen, Anda perlu mengimpor komponen-komponen tersebut dan membuat rute di dalam App.js. Lihat tutorial Cara Menangani Routing di Aplikasi React dengan React Router untuk pengenalan lengkap tentang routing di aplikasi React. Untuk menerapkan aplikasi React Anda, lihat Cara Menerapkan Aplikasi React dengan Nginx di Ubuntu.
Untuk memulai, buka App.js:
nano src/components/App/App.jsKemudian impor Dashboard dan Preferences dengan menambahkan kode yang disorot berikut:
import React from 'react';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';
function App() {
return (
<></>
);
}
export default App;Selanjutnya, impor BrowserRouter, Routes, dan Route dari react-router-dom. React Router v6 menggunakan Routes menggantikan Switch, dan rute menggunakan properti element bukan children:
import React from 'react';
import './App.css';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';
function App() {
return (
<></>
);
}
export default App;Tambahkan <div> di sekelilingnya dengan className wrapper dan tag <h1> untuk dijadikan sebagai template aplikasi. Pastikan Anda mengimpor App.css agar dapat menerapkan gaya.
Selanjutnya, buat rute untuk komponen Dashboard dan Preferences. Tambahkan BrowserRouter, lalu tambahkan komponen Routes sebagai anak. Di dalam Routes, tambahkan Route dengan properti path dan element untuk setiap komponen:
import React from 'react';
import './App.css';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';
function App() {
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/preferences" element={<Preferences />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Simpan dan tutup berkas.
Langkah terakhir adalah menambahkan sedikit padding ke <div> utama agar komponen Anda tidak langsung berada di tepi browser. Untuk melakukan ini, Anda akan mengubah CSS.
Buka App.css:
nano src/components/App/App.cssGanti kontennya dengan kelas .wrapper dengan padding sebesar 20px:
.wrapper {
padding: 20px;
}Simpan dan tutup file. Peramban akan memuat ulang secara otomatis, menampilkan komponen dasar:
Kunjungi http://localhost:3000/dashboard untuk melihat halaman dashboard:
Rute-rute tersebut berfungsi, tetapi /dashboard dan /preferences saat ini dapat diakses oleh siapa saja, termasuk pengguna yang belum diautentikasi. Rute-rute ini seharusnya dilindungi.
Dua pendekatan untuk melindungi rute:
Alihkan ke rute login khusus: Buat rute
/logindan alihkan pengguna yang belum terautentikasi ke sana. Ini berhasil, tetapi pengguna kehilangan tujuan yang ingin dicapai dan harus menavigasi kembali setelah login.Render formulir login di rute mana pun (digunakan dalam tutorial ini): Tampilkan formulir login di rute saat ini ketika token tidak ada. Setelah autentikasi, pengguna tetap berada di tujuan yang mereka inginkan. Misalnya, mengunjungi
/dashboardsaat belum login akan menampilkan formulir login, dan setelah login, pengguna langsung sampai di/dashboard.
Tutorial ini menggunakan pendekatan kedua untuk pengalaman pengguna yang lebih baik.
Untuk memulai, buat direktori baru untuk komponen Login:
mkdir src/components/LoginSelanjutnya, buka Login.js di editor teks:
nano src/components/Login/Login.jsBuatlah formulir dasar dengan tombol <button> untuk mengirim dan <input> untuk nama pengguna dan kata sandi. Pastikan untuk mengatur tipe input untuk kata sandi menjadi password:
import React from 'react';
export default function Login() {
return(
<form>
<label>
<p>Username</p>
<input type="text" />
</label>
<label>
<p>Password</p>
<input type="password" />
</label>
<div>
<button type="submit">Submit</button>
</div>
</form>
)
}Untuk informasi lebih lanjut tentang formulir di React, lihat tutorial Cara Membuat Formulir di React. Untuk mempelajari tentang mengelola state di komponen React, lihat Cara Mengelola State dengan Hooks pada Komponen React.
Selanjutnya, tambahkan tag <h1> yang meminta pengguna untuk masuk. Bungkus <form> dan <h1> dalam <div> dengan className login-wrapper. Terakhir, impor Login.css:
import React from 'react';
import './Login.css';
export default function Login() {
return(
<div className="login-wrapper">
<h1>Please Log In</h1>
<form>
<label>
<p>Username</p>
<input type="text" />
</label>
<label>
<p>Password</p>
<input type="password" />
</label>
<div>
<button type="submit">Submit</button>
</div>
</form>
</div>
)
}Simpan dan tutup berkas.
Tambahkan styling untuk memusatkan form login. Buka Login.css:
nano src/components/Login/Login.cssPusatkan komponen di halaman dengan menambahkan display dengan nilai flex, kemudian atur flex-direction ke column untuk menyusun elemen secara vertikal dan tambahkan align-items ke center agar komponen berada di tengah browser:
.login-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}Untuk informasi lebih lanjut tentang penggunaan Flexbox, lihat Cheatsheet CSS Flexbox kami
Simpan dan tutup berkas.
Render komponen login di App.js ketika tidak ada token autentikasi. Buka App.js:
nano src/components/App/App.jsDi Langkah 3, Anda akan mengimplementasikan penyimpanan token secara persisten. Untuk sementara, simpan token di state komponen menggunakan useState Hook untuk menguji alur login. Perlu dicatat bahwa token yang disimpan di state komponen akan hilang saat halaman di-refresh.
Impor useState dari react, kemudian panggil useState dan atur nilai kembalian ke token dan setToken:
import React, { useState } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';
function App() {
const [token, setToken] = useState();
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/preferences" element={<Preferences />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Impor komponen Login. Tambahkan pernyataan kondisional untuk menampilkan Login jika token bernilai falsy.
Serahkan fungsi setToken ke komponen Login:
import React, { useState } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
function App() {
const [token, setToken] = useState();
if(!token) {
return <Login setToken={setToken} />
}
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/preferences" element={<Preferences />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Karena belum ada token yang tersedia, formulir login ditampilkan di semua rute. Pada langkah berikutnya, Anda akan membuat endpoint API yang mengembalikan token setelah autentikasi berhasil.
Simpan dan tutup file. Browser akan memuat ulang secara otomatis, menampilkan halaman login di semua rute. Kunjungi halaman dashboard untuk memastikan formulir login muncul:
Anda telah membuat formulir login yang ditampilkan di semua rute ketika tidak ada token autentikasi. Formulir ini mencegah akses ke rute yang dilindungi seperti /dashboard dan /preferences sampai autentikasi berhasil. Pada langkah berikutnya, Anda akan membuat server API tiruan yang mengembalikan token autentikasi.
Langkah 2 — Membuat API Token
Mock API mensimulasikan autentikasi backend selama pengembangan tanpa memerlukan server produksi. Express menyediakan cara sederhana untuk membuat server pengembangan yang mengembalikan token dalam format yang sama dengan yang akan digunakan API produksi Anda. Ini memungkinkan Anda menguji alur autentikasi, menangani respons API, dan mengembangkan frontend secara mandiri.
Mengapa menggunakan API tiruan: Selama pengembangan, Anda mungkin belum memiliki backend yang siap, atau Anda ingin menguji alur autentikasi tanpa mengakses server produksi. API tiruan memberikan respons yang dapat diprediksi, sehingga lebih mudah untuk menguji penanganan kesalahan, status pemuatan, dan manajemen token. Untuk aplikasi produksi, gantilah API tiruan dengan endpoint autentikasi Anda yang sebenarnya.
Buat server Express sederhana yang mengembalikan token. Untuk pengantar lengkap tentang Express, lihat Server Express Dasar di Node.js. Jika Anda sedang membangun aplikasi full-stack, pertimbangkan Cara Membuat Aplikasi To-Do Menggunakan Django dan React untuk contoh lengkap.
Untuk memulai, instal express. Karena server bukanlah persyaratan dari build akhir, pastikan untuk menginstalnya sebagai devDependency.
Anda juga perlu menginstal cors. Perpustakaan ini akan memungkinkan berbagi sumber daya lintas asal untuk semua rute.
Peringatan: Jangan mengaktifkan CORS untuk semua rute dalam aplikasi produksi. Hal ini dapat menyebabkan kerentanan keamanan.
npm install --save-dev express corsInstalasi selesai dengan output yang mirip dengan:
...
+ cors@2.8.5
+ express@4.17.1
removed 10 packages, updated 2 packages and audited 2059 packages in 12.597s
...Selanjutnya, buka file baru bernama server.js di root aplikasi Anda. Jangan menambahkan file ini ke direktori /src karena Anda tidak ingin itu menjadi bagian dari build akhir.
nano server.jsImpor express, kemudian inisialisasi aplikasi baru dengan memanggil express() dan menyimpan hasilnya ke dalam variabel bernama app:
const express = require('express');
const app = express();Setelah membuat app, tambahkan cors sebagai middleware. Pertama, impor cors, lalu tambahkan ke aplikasi dengan memanggil metode use pada app:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());Tambahkan penangan rute untuk /login yang mengembalikan token. Penangan menerima req (data permintaan) dan res (objek respons). Gunakan res.send untuk mengembalikan objek JavaScript yang berisi token:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.use('/login', (req, res) => {
res.send({
token: 'test123'
});
});Terakhir, jalankan server di port 8080 menggunakan app.listen:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.use('/login', (req, res) => {
res.send({
token: 'test123'
});
});
app.listen(8080, () => console.log('API is running on http://localhost:8080'));Simpan dan tutup file. Di jendela atau tab terminal baru, jalankan server:
node server.jsServer dimulai dan menampilkan:
API is running on http://localhost:8080/loginKunjungi halaman login di browser Anda untuk melihat respons objek JSON:
Catatan: Mengunjungi URL di browser mengirimkan permintaan GET, sedangkan formulir login akan mengirimkan permintaan POST. Penangan rute saat ini menerima kedua metode tersebut. Di lingkungan produksi, batasi rute ke metode HTTP tertentu menggunakan app.post() untuk permintaan POST dan app.get() untuk permintaan GET. Lihat dokumentasi routing Express untuk detailnya.
Sekarang setelah Anda memiliki server API yang berjalan, Anda perlu membuat permintaan dari halaman login Anda. Buka Login.js:
nano src/components/Login/Login.jsPada langkah sebelumnya, Anda mengirimkan prop baru bernama setToken ke komponen Login. Tambahkan PropType dari prop baru tersebut dan destruktur objek props untuk mengambil prop setToken.
import React from 'react';
import PropTypes from 'prop-types';
import './Login.css';
export default function Login({ setToken }) {
return(
<div className="login-wrapper">
<h1>Please Log In</h1>
<form>
<label>
<p>Username</p>
<input type="text" />
</label>
<label>
<p>Password</p>
<input type="password" />
</label>
<div>
<button type="submit">Submit</button>
</div>
</form>
</div>
)
}
Login.propTypes = {
setToken: PropTypes.func.isRequired
}Selanjutnya, buat state lokal untuk menangkap Username dan Password. Karena Anda tidak perlu mengatur data secara manual, jadikan <inputs> komponen yang tidak terkendali. Anda bisa menemukan informasi lebih lengkap tentang komponen yang tidak terkendali di Cara Membangun Formulir di React.
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';
export default function Login({ setToken }) {
const [username, setUserName] = useState();
const [password, setPassword] = useState();
return(
<div className="login-wrapper">
<h1>Please Log In</h1>
<form>
<label>
<p>Username</p>
<input type="text" onChange={e => setUserName(e.target.value)}/>
</label>
<label>
<p>Password</p>
<input type="password" onChange={e => setPassword(e.target.value)}/>
</label>
<div>
<button type="submit">Submit</button>
</div>
</form>
</div>
)
}
Login.propTypes = {
setToken: PropTypes.func.isRequired
};Buat fungsi untuk melakukan POST request ke server. Dalam aplikasi yang lebih besar, letakkan panggilan API di direktori layanan terpisah. Untuk tutorial ini, fungsi tersebut didefinisikan di file komponen. Untuk lebih lanjut tentang panggilan API di React, lihat Cara Memanggil Web API dengan Hook useEffect di React.
Buat fungsi async yang bernama loginUser. Fungsi ini akan mengambil credentials sebagai argumen, kemudian akan memanggil metode fetch menggunakan opsi POST:
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';
async function loginUser(credentials) {
return fetch('http://localhost:8080/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
})
.then(data => data.json())
}
export default function Login({ setToken }) {
...Terakhir, buat handler pengiriman form yang disebut handleSubmit yang akan memanggil loginUser dengan username dan password. Panggil setToken dengan hasil yang berhasil. Panggil handleSubmit menggunakan handler event onSubmit pada <form>:
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';
async function loginUser(credentials) {
return fetch('http://localhost:8080/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
})
.then(data => data.json())
}
export default function Login({ setToken }) {
const [username, setUserName] = useState();
const [password, setPassword] = useState();
const handleSubmit = async e => {
e.preventDefault();
const token = await loginUser({
username,
password
});
setToken(token);
}
return(
<div className="login-wrapper">
<h1>Please Log In</h1>
<form onSubmit={handleSubmit}>
<label>
<p>Username</p>
<input type="text" onChange={e => setUserName(e.target.value)} />
</label>
<label>
<p>Password</p>
<input type="password" onChange={e => setPassword(e.target.value)} />
</label>
<div>
<button type="submit">Submit</button>
</div>
</form>
</div>
)
}
Login.propTypes = {
setToken: PropTypes.func.isRequired
};Catatan: Dalam aplikasi produksi, tangani pelepasan komponen saat operasi async. Jika pengguna meninggalkan halaman sebelum Promise selesai, Anda mungkin perlu membatalkan permintaan atau mengabaikan responsnya. Lihat Cara Memanggil Web API dengan Hook useEffect di React untuk pola pembersihan.
Simpan dan tutup berkas. Pastikan server API masih berjalan, lalu kunjungi http://localhost:3000/dashboard di browser Anda.
Halaman login muncul. Masukkan nama pengguna dan kata sandi apa saja, lalu kirimkan formulirnya. API mengembalikan token, dan aplikasi mengarahkan ke dashboard.
Alur login berfungsi: mengirimkan formulir memanggil API dan menyimpan token yang dikembalikan. Namun, token disimpan dalam state komponen (memori JavaScript), yang berarti token hilang saat halaman di-refresh atau saat membuka tab baru. Pengguna perlu login lagi setelah setiap refresh.
Masalah: State komponen (useState) tidak bertahan saat halaman di-refresh. Ketika browser melakukan refresh, React menginisialisasi ulang komponen, dan semua state yang disimpan di memori hilang.
Pada langkah berikutnya, Anda akan mengimplementasikan penyimpanan token secara persisten menggunakan API penyimpanan browser (localStorage atau sessionStorage) sehingga autentikasi tetap berlaku meskipun halaman direfresh dan di berbagai sesi browser.
Langkah 3 — Menyimpan Token Pengguna dengan sessionStorage dan localStorage
Lokasi penyimpanan token menentukan persistensi sesi dan kompromi keamanan. localStorage menjaga pengguna tetap masuk di berbagai sesi dan tab browser, sementara sessionStorage akan terhapus saat tab ditutup. Keduanya dapat diakses oleh JavaScript, sehingga rentan terhadap serangan XSS, tetapi menyediakan pengalaman pengguna yang lebih baik dibandingkan menyimpan token hanya di state komponen.
Perbandingan opsi penyimpanan:
- Status komponen (
useState): Hilang saat halaman disegarkan. Tidak ada penyimpanan permanen. sessionStorage: Bertahan selama sesi tab browser. Dihapus saat tab ditutup. Tidak dibagikan antar tab.localStorage: Bertahan di seluruh sesi dan tab browser. Tetap ada setelah browser dimulai ulang. Dibagikan di semua tab dari asal yang sama.- Cookies: Secara otomatis dikirim dengan permintaan. Membutuhkan perlindungan CSRF. Dapat
httpOnly(tidak dapat diakses oleh JavaScript, lebih aman).
Pertimbangan keamanan: Setiap data yang disimpan di luar memori komponen (termasuk localStorage, sessionStorage, dan cookie yang dapat diakses oleh JavaScript) rentan terhadap serangan Cross-Site Scripting (XSS). Jika kode berbahaya dijalankan di domain Anda, kode tersebut dapat mengakses mekanisme penyimpanan ini. Selalu bersihkan input dari pengguna dan gunakan header Content Security Policy (CSP) untuk mengurangi risiko XSS.
Tutorial ini membahas sessionStorage dan localStorage sebagai alternatif modern untuk cookies dalam penyimpanan token. Cookies dibahas di Langkah 8 saat membahas perlindungan CSRF.
Penyimpanan Sesi
Untuk menguji manfaat menyimpan di luar memori, ubah penyimpanan dalam memori menjadi sessionStorage. Buka App.js:
nano src/components/App/App.jsHapus pemanggilan useState dan buat dua fungsi baru yang disebut setToken dan getToken. Kemudian panggil getToken dan tetapkan hasilnya ke variabel yang disebut token:
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
function setToken(userToken) {
}
function getToken() {
}
function App() {
const token = getToken();
if(!token) {
return <Login setToken={setToken} />
}
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/preferences" element={<Preferences />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Menggunakan nama fungsi yang sama (setToken, getToken) dan nama variabel (token) berarti komponen Login dan sisa App.js tidak perlu diubah. Antar muka tetap sama, tetapi implementasinya sekarang menggunakan penyimpanan yang persisten.
Implementasikan setToken untuk menyimpan token ke sessionStorage. Metode setItem membutuhkan sebuah kunci (string) dan nilai (string), jadi ubah objek userToken menjadi string JSON menggunakan JSON.stringify:
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
function setToken(userToken) {
sessionStorage.setItem('token', JSON.stringify(userToken));
}
function getToken() {
}
function App() {
const token = getToken();
if(!token) {
return <Login setToken={setToken} />
}
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/preferences" element={<Preferences />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Simpan file tersebut. Browser akan memuat ulang secara otomatis. Setelah mengirimkan formulir login, token akan disimpan di sessionStorage, tetapi halaman login masih ditampilkan karena React belum melakukan re-render. Buka alat pengembang browser Anda (F12) dan periksa tab Storage untuk memverifikasi token telah tersimpan. Gambar berikut menunjukkan alat pengembang Firefox; Chrome dan browser modern lainnya memiliki alat inspeksi penyimpanan yang serupa.
Implementasikan getToken untuk mengambil token dari sessionStorage. Metode getItem mengembalikan string yang disimpan (atau null jika tidak ada nilai). Ubah string JSON kembali menjadi objek, lalu kembalikan properti token:
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
function setToken(userToken) {
sessionStorage.setItem('token', JSON.stringify(userToken));
}
function getToken() {
const tokenString = sessionStorage.getItem('token');
if (!tokenString) {
return null;
}
const userToken = JSON.parse(tokenString);
return userToken?.token;
}
function App() {
const token = getToken();
if(!token) {
return <Login setToken={setToken} />
}
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/preferences" element={<Preferences />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Mengapa menggunakan optional chaining (?.): Pada kunjungan pertama, sessionStorage.getItem('token') mengembalikan null, dan JSON.parse(null) mengembalikan null. Tanpa optional chaining, mengakses userToken.token ketika userToken adalah null akan memunculkan TypeError. Operator ?. dengan aman mengembalikan undefined alih-alih memunculkan kesalahan.
Simpan dan tutup file. Jika token ada di sessionStorage, browser akan secara otomatis menavigasi ke halaman yang dilindungi saat menyegarkan:
Untuk menguji alur login lagi, hapus token yang tersimpan. Di alat pengembang browser Anda, hapus token dari tab Storage atau jalankan sessionStorage.clear() di konsol.
Masalah: Setelah masuk, token tersimpan di sessionStorage, tetapi halaman login masih ditampilkan. Ini terjadi karena React tidak mengetahui bahwa token telah disimpan, dan komponen perlu dirender ulang ketika token berubah.
Mengapa React tidak melakukan re-render: Menyimpan data di sessionStorage tidak memicu re-render di React. React hanya melakukan re-render saat state berubah. Untuk memperbaikinya, gunakan state React untuk melacak token, dan perbarui state tersebut saat menyimpan atau mengambil token. Ini akan memicu re-render dan memperbarui UI.
Solusi: Buat Hook custom yang menggabungkan operasi penyimpanan dengan state React. Ini memusatkan logika manajemen token dan membuatnya bisa digunakan kembali di berbagai komponen.
Membuat Hook Token Kustom
Apa itu custom Hook: Custom Hook adalah fungsi JavaScript yang menggunakan React Hooks (seperti useState, useEffect) dan membungkus logika yang bisa digunakan ulang. Sesuai konvensi, custom Hooks diawali dengan use* (misalnya, useToken, useAuth). Custom Hooks memungkinkan Anda mengekstrak logika komponen menjadi fungsi yang dapat digunakan kembali, sehingga kode menjadi lebih mudah dipelihara dan diuji.
Buka file baru di direktori App yang bernama useToken.js:
nano src/components/App/useToken.jsOrganisasi file: Hook ini bisa didefinisikan langsung di App.js, tetapi mengekstraknya ke file terpisah menunjukkan bagaimana Hook kustom bekerja secara independen dari komponen. Saat aplikasi Anda berkembang dan Anda menggunakan kembali Hook ini di beberapa komponen, menyimpannya di file terpisah akan memudahkan pemeliharaan dan pengujian.
Di useToken.js, impor useState dari react. Karena file ini tidak mengandung JSX, Anda tidak perlu mengimpor React. Buat dan ekspor sebuah fungsi bernama useToken yang menggunakan useState untuk mengelola status token:
import { useState } from 'react';
export default function useToken() {
const [token, setToken] = useState();
}Tambahkan fungsi getToken sebagai fungsi panah di dalam useToken. Letakkan sebelum deklarasi state, lalu inisialisasi useState dengan getToken() untuk memuat token yang ada saat komponen dipasang:
import { useState } from 'react';
export default function useToken() {
const getToken = () => {
const tokenString = sessionStorage.getItem('token');
if (!tokenString) {
return null;
}
const userToken = JSON.parse(tokenString);
return userToken?.token;
};
const [token, setToken] = useState(getToken());
}Tambahkan fungsi saveToken yang menyimpan token di sessionStorage dan state React. Menyimpan ke state akan memicu re-render, memperbarui UI:
import { useState } from 'react';
export default function useToken() {
const getToken = () => {
const tokenString = sessionStorage.getItem('token');
if (!tokenString) {
return null;
}
const userToken = JSON.parse(tokenString);
return userToken?.token;
};
const [token, setToken] = useState(getToken());
const saveToken = userToken => {
sessionStorage.setItem('token', JSON.stringify(userToken));
setToken(userToken.token);
};
}Kembalikan sebuah objek dengan token dan setToken (dipetakan ke saveToken). Menggunakan objek daripada array memungkinkan komponen untuk hanya mengambil nilai yang mereka butuhkan. Ini mempertahankan antarmuka yang sama seperti sebelumnya, sehingga App.js tidak perlu diubah:
import { useState } from 'react';
export default function useToken() {
const getToken = () => {
const tokenString = sessionStorage.getItem('token');
if (!tokenString) {
return null;
}
const userToken = JSON.parse(tokenString);
return userToken?.token;
};
const [token, setToken] = useState(getToken());
const saveToken = userToken => {
sessionStorage.setItem('token', JSON.stringify(userToken));
setToken(userToken.token);
};
return {
setToken: saveToken,
token
}
}Simpan dan tutup berkas.
Selanjutnya, buka App.js:
nano src/components/App/App.jsHapus fungsi getToken dan setToken dari App.js, dan hapus impor useState. Impor dan gunakan Hook useToken sebagai gantinya:
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
import useToken from './useToken';
function App() {
const { token, setToken } = useToken();
if(!token) {
return <Login setToken={setToken} />
}
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/preferences" element={<Preferences />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Simpan dan tutup file. Browser akan menyegarkan secara otomatis. Setelah masuk, aplikasi langsung menuju halaman yang dilindungi karena useState di Hook khusus memicu render ulang ketika status token diperbarui:
Hook kustom menyimpan token di sessionStorage dan mengelola state React. Menyegarkan halaman membuat pengguna tetap masuk karena token tetap ada di sessionStorage. Namun, membuka aplikasi di tab baru akan membuat pengguna keluar karena sessionStorage bersifat spesifik untuk tab, dan data tidak dibagikan antar tab serta akan dihapus saat tab ditutup.
Kapan menggunakan sessionStorage: Gunakan sessionStorage ketika Anda menginginkan autentikasi hanya untuk sesi yang akan terhapus saat tab ditutup. Ini lebih aman untuk komputer yang digunakan bersama.
Kapan menggunakan localStorage: Gunakan localStorage ketika Anda ingin autentikasi bertahan di berbagai tab dan sesi browser. Ini memberikan pengalaman pengguna yang lebih baik tetapi mengharuskan pengguna untuk keluar secara eksplisit di komputer bersama.
Menggunakan localStorage untuk Menyimpan Data di Beberapa Jendela
localStorage bertahan di berbagai sesi dan tab browser, tidak seperti sessionStorage yang spesifik untuk tab. Ini berarti pengguna tetap masuk saat membuka tab baru atau memulai ulang browser.
Pertimbangan keamanan: Pada komputer bersama, token localStorage tetap ada bahkan setelah menutup browser. Pengguna berikutnya dapat mengakses aplikasi tanpa masuk jika pengguna sebelumnya tidak keluar. Untuk komputer bersama, pertimbangkan:
- Menambahkan peringatan waktu habis sesi
- Mengimplementasikan “Ingat Saya” sebagai fitur pilihan
- Menggunakan
sessionStoragesecara default danlocalStoragehanya jika diminta secara eksplisit - Menyediakan fungsi logout yang jelas
Untuk perangkat pribadi, localStorage memberikan pengalaman pengguna yang lebih baik dengan risiko keamanan minimal ketika dikombinasikan dengan perlindungan XSS yang tepat.
Untuk mengonversi ke localStorage, buka useToken.js:
nano src/components/App/useToken.jsGanti semua referensi sessionStorage dengan localStorage. Metode API (getItem, setItem) identik:
import { useState } from 'react';
export default function useToken() {
const getToken = () => {
const tokenString = localStorage.getItem('token');
if (!tokenString) {
return null;
}
const userToken = JSON.parse(tokenString);
return userToken?.token;
};
const [token, setToken] = useState(getToken());
const saveToken = userToken => {
localStorage.setItem('token', JSON.stringify(userToken));
setToken(userToken.token);
};
return {
setToken: saveToken,
token
}
}Simpan berkasnya. Browser akan menyegarkan secara otomatis. Masuk kembali (token sebelumnya ada di sessionStorage, bukan localStorage). Setelah masuk, token akan tersimpan di localStorage, sehingga Anda tetap masuk saat membuka tab baru atau memulai ulang browser.
Anda telah mengimplementasikan penyimpanan token yang persisten menggunakan sessionStorage dan localStorage, serta membuat Hook kustom useToken yang menggabungkan operasi penyimpanan dengan manajemen state di React. Hook ini memicu re-render saat token berubah, memastikan UI diperbarui dengan benar. Anda juga telah mempelajari perbandingan antara sessionStorage (khusus tab, lebih aman) dan localStorage (bertahan lintas sesi, pengalaman pengguna lebih baik).
Langkah 4 — Menerapkan Rute Terlindungi dengan React Router v6
Rute yang dilindungi memerlukan autentikasi sebelum ditampilkan. Dalam React Router v6, Anda membuat komponen pembungkus yang memeriksa token dan mengarahkan pengguna yang belum terautentikasi ke halaman login, sambil mempertahankan tujuan yang ingin mereka tuju.
Mengapa menggunakan komponen ProtectedRoute: Alih-alih memeriksa autentikasi di setiap komponen atau menggunakan render kondisional di App.js, komponen ProtectedRoute khusus memusatkan logika perlindungan rute. Ini membuat lebih mudah untuk menambahkan rute yang dilindungi, menangani pengalihan, dan mempertahankan perilaku autentikasi yang konsisten.
Buat file baru untuk komponen ProtectedRoute:
nano src/components/ProtectedRoute/ProtectedRoute.jsDi dalam ProtectedRoute.js, import Navigate dari react-router-dom dan buat sebuah komponen yang memeriksa token:
import { Navigate } from 'react-router-dom';
export default function ProtectedRoute({ children, token }) {
if (!token) {
return <Navigate to="/login" replace />;
}
return children;
}Komponen ProtectedRoute menerima children (komponen rute yang ingin dilindungi) dan token sebagai props. Jika tidak ada token, komponen ini akan mengarahkan ke /login menggunakan Navigate dengan props replace untuk menghindari menambahkan entri ke riwayat browser.
Sekarang perbarui App.js untuk menggunakan ProtectedRoute:
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
import ProtectedRoute from '../ProtectedRoute/ProtectedRoute';
import useToken from './useToken';
function App() {
const { token, setToken } = useToken();
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login setToken={setToken} />} />
<Route
path="/dashboard"
element={
<ProtectedRoute token={token}>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/preferences"
element={
<ProtectedRoute token={token}>
<Preferences />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Dengan pendekatan ini, pengguna yang tidak terautentikasi yang mengunjungi /dashboard atau /preferences akan diarahkan ke /login.
Catatan: Di React Router v6, Anda juga dapat menggunakan komponen Outlet untuk rute bersarang dan perlindungan rute. Untuk aplikasi yang lebih sederhana, komponen pembungkus ProtectedRoute yang ditunjukkan di sini sudah cukup dan lebih mudah dipahami.
Langkah 5 — Mendekode Token JWT dan Memeriksa Masa Berlaku
Token JWT mengandung informasi kedaluwarsa di dalam payload-nya. Anda dapat mendekode token di sisi klien untuk memeriksa apakah token tersebut telah kedaluwarsa tanpa harus membuat permintaan ke server, sehingga meningkatkan pengalaman pengguna dengan menampilkan umpan balik secara langsung.
Mengapa mendekode token di sisi klien: Meskipun Anda selalu harus memvalidasi token di server, mendekode di sisi klien memungkinkan Anda memeriksa masa berlaku sebelum melakukan panggilan API, menampilkan pesan kesalahan yang sesuai, dan secara otomatis keluar dari akun pengguna dengan token yang telah kedaluwarsa.
Pertama, pasang pustaka decoding JWT. Meskipun Anda bisa mendekode JWT secara manual (karena mereka dikodekan dengan base64), menggunakan pustaka menangani kasus-kasus khusus dan memberikan penanganan kesalahan yang lebih baik:
npm install jwt-decodePerbarui useToken.js hook Anda untuk mendekode dan memvalidasi token:
import { useState } from 'react';
import { jwtDecode } from 'jwt-decode';
export default function useToken() {
const getToken = () => {
const tokenString = localStorage.getItem('token');
if (!tokenString) {
return null;
}
const userToken = JSON.parse(tokenString);
const token = userToken?.token;
if (!token) {
return null;
}
// Decode the token to check expiration
try {
const decoded = jwtDecode(token);
const currentTime = Date.now() / 1000; // Convert to seconds
// If token is expired, remove it and return null
if (decoded.exp < currentTime) {
localStorage.removeItem('token');
return null;
}
return token;
} catch (error) {
// If decoding fails, token is invalid
localStorage.removeItem('token');
return null;
}
};
const [token, setToken] = useState(getToken());
const saveToken = userToken => {
localStorage.setItem('token', JSON.stringify(userToken));
setToken(userToken.token);
};
const removeToken = () => {
localStorage.removeItem('token');
setToken(null);
};
return {
setToken: saveToken,
token,
removeToken
};
}Fungsi getToken sekarang mendekode JWT dan memeriksa klaim exp (timestamp kedaluwarsa dalam detik). Jika token sudah kedaluwarsa atau tidak valid, token tersebut akan dihapus dari penyimpanan dan mengembalikan null, secara efektif keluar dari akun pengguna.
Peringatan: Dekode token di sisi klien hanya untuk pengalaman pengguna (UX). Selalu validasi token di server Anda. Pengguna jahat dapat memodifikasi kode sisi klien atau mengirim token palsu. Jangan pernah mempercayai validasi sisi klien untuk keputusan keamanan.
Langkah 6 — Menerapkan Logout dan Penanganan Kesalahan
Logout menghapus token dari penyimpanan dan mengarahkan kembali ke halaman login. Penanganan error mencegah aplikasi rusak saat panggilan API gagal dan memberikan umpan balik kepada pengguna.
Mengapa perlu menerapkan logout: Tanpa fungsi logout, pengguna harus menghapus penyimpanan browser secara manual atau menunggu token kedaluwarsa. Tombol logout memberikan cara yang jelas untuk mengakhiri sesi dan penting untuk komputer yang digunakan bersama.
Perbarui komponen Dashboard Anda untuk menyertakan tombol keluar:
import React from 'react';
import { useNavigate } from 'react-router-dom';
export default function Dashboard({ removeToken }) {
const navigate = useNavigate();
const handleLogout = () => {
removeToken();
navigate('/login');
};
return (
<div>
<h2>Dashboard</h2>
<button onClick={handleLogout}>Logout</button>
</div>
);
}Perbarui App.js untuk meneruskan removeToken ke rute yang dilindungi:
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
import ProtectedRoute from '../ProtectedRoute/ProtectedRoute';
import useToken from './useToken';
function App() {
const { token, setToken, removeToken } = useToken();
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login setToken={setToken} />} />
<Route
path="/dashboard"
element={
<ProtectedRoute token={token}>
<Dashboard removeToken={removeToken} />
</ProtectedRoute>
}
/>
<Route
path="/preferences"
element={
<ProtectedRoute token={token}>
<Preferences />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
</div>
);
}
export default App;Sekarang tambahkan penanganan kesalahan ke komponen login:
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';
async function loginUser(credentials) {
const response = await fetch('http://localhost:8080/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
});
if (!response.ok) {
throw new Error(`Login failed: ${response.statusText}`);
}
const data = await response.json();
return data;
}
export default function Login({ setToken }) {
const [username, setUserName] = useState();
const [password, setPassword] = useState();
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const handleSubmit = async e => {
e.preventDefault();
setError(null);
setLoading(true);
try {
const token = await loginUser({
username,
password
});
setToken(token);
} catch (err) {
setError(err.message || 'An error occurred during login');
} finally {
setLoading(false);
}
};
return(
<div className="login-wrapper">
<h1>Please Log In</h1>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<label>
<p>Username</p>
<input
type="text"
onChange={e => setUserName(e.target.value)}
disabled={loading}
/>
</label>
<label>
<p>Password</p>
<input
type="password"
onChange={e => setPassword(e.target.value)}
disabled={loading}
/>
</label>
<div>
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Submit'}
</button>
</div>
</form>
</div>
);
}
Login.propTypes = {
setToken: PropTypes.func.isRequired
};Komponen login sekarang menangani kesalahan, menampilkan status pemuatan, dan memberikan umpan balik kepada pengguna. Penanganan kesalahan mencegah aplikasi dari kerusakan saat panggilan API gagal dan memberikan umpan balik kepada pengguna tentang apa yang salah.
Langkah 7 — Menggunakan React Context untuk Autentikasi
React Context menyediakan cara untuk berbagi status autentikasi di seluruh komponen tanpa harus meneruskan props secara berlapis. Untuk aplikasi yang lebih besar, Context lebih mudah diskalakan dibandingkan harus meneruskan props melalui beberapa lapisan komponen.
Mengapa menggunakan Context untuk autentikasi: Ketika status autentikasi dibutuhkan di banyak komponen (bilah navigasi, menu pengguna, rute yang dilindungi), meneruskan props menjadi merepotkan. Context menyediakan cara yang bersih untuk mengakses status autentikasi di mana saja dalam pohon komponen.
Buat konteks autentikasi:
import React, { createContext, useContext, useState } from 'react';
import useToken from '../components/App/useToken';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const { token, setToken, removeToken } = useToken();
const [user, setUser] = useState(null);
const login = async (credentials) => {
// Login logic here
const response = await fetch('http://localhost:8080/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
});
if (!response.ok) {
throw new Error('Login failed');
}
const data = await response.json();
setToken(data);
// Optionally decode token to set user info
return data;
};
const logout = () => {
removeToken();
setUser(null);
};
const value = {
token,
user,
login,
logout,
isAuthenticated: !!token
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}Bungkus aplikasi Anda dengan AuthProvider:
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { AuthProvider } from '../../contexts/AuthContext';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
import ProtectedRoute from '../ProtectedRoute/ProtectedRoute';
function App() {
return (
<AuthProvider>
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/preferences"
element={
<ProtectedRoute>
<Preferences />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
</div>
</AuthProvider>
);
}
export default App;Perbarui ProtectedRoute untuk menggunakan konteks:
import { Navigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
export default function ProtectedRoute({ children }) {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return children;
}Sekarang setiap komponen dapat mengakses status autentikasi menggunakan useAuth(), sehingga tidak perlu lagi melewatkan props melalui beberapa lapisan.
Langkah 8 — Memahami Perlindungan CSRF untuk Otentikasi Berbasis Cookie
Serangan CSRF (Cross-Site Request Forgery) memanfaatkan fakta bahwa browser secara otomatis mengirim cookie dengan permintaan. Saat menggunakan autentikasi berbasis cookie, Anda harus menerapkan perlindungan CSRF untuk mencegah situs jahat melakukan permintaan yang diautentikasi atas nama pengguna.
Kenapa perlindungan CSRF penting: Jika autentikasi Anda bergantung pada cookie (alih-alih token di localStorage), situs web jahat bisa menipu pengguna untuk membuat permintaan yang terautentikasi ke API Anda. Misalnya, seorang penyerang bisa menyematkan sebuah form yang dikirim ke API Anda, dan jika pengguna sudah login, browser secara otomatis menyertakan cookie autentikasi.
Strategi perlindungan CSRF:
Token CSRF: Buat token unik untuk setiap sesi dan wajibkan penggunaannya dalam permintaan. Token harus cocok dengan yang disimpan di server.
Atribut Cookie SameSite: Setel cookie dengan
SameSite=StrictatauSameSite=Laxuntuk mencegahnya dikirim dalam permintaan lintas situs.Verifikasi Header Origin: Periksa bahwa header
OriginatauReferersesuai dengan domain Anda.Cookie Double Submit: Simpan token CSRF baik di cookie maupun di bidang formulir, dan keduanya harus cocok.
Berikut adalah contoh penerapan perlindungan CSRF di server Express Anda:
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const app = express();
app.use(cors({
origin: 'http://localhost:3000',
credentials: true // Allow cookies
}));
app.use(cookieParser());
app.use(express.json());
// Generate CSRF token
app.get('/api/csrf-token', (req, res) => {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrf-token', token, {
httpOnly: false, // Must be accessible to JavaScript
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production'
});
res.json({ csrfToken: token });
});
// Verify CSRF token middleware
function verifyCsrfToken(req, res, next) {
const token = req.headers['x-csrf-token'];
const cookieToken = req.cookies['csrf-token'];
if (!token || !cookieToken || token !== cookieToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
}
app.post('/login', verifyCsrfToken, (req, res) => {
// Login logic here
res.json({ token: 'test123' });
});
app.listen(8080, () => console.log('API is running on http://localhost:8080'));Dalam aplikasi React Anda, ambil token CSRF sebelum melakukan permintaan yang diautentikasi:
let csrfToken = null;
export async function getCsrfToken() {
if (!csrfToken) {
const response = await fetch('http://localhost:8080/api/csrf-token', {
credentials: 'include' // Include cookies
});
const data = await response.json();
csrfToken = data.csrfToken;
}
return csrfToken;
}
export async function loginUser(credentials) {
const token = await getCsrfToken();
return fetch('http://localhost:8080/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
credentials: 'include', // Include cookies
body: JSON.stringify(credentials)
}).then(data => data.json());
}Catatan: Perlindungan CSRF tidak diperlukan untuk autentikasi berbasis token yang disimpan di localStorage karena token harus secara eksplisit disertakan dalam header permintaan dan tidak dikirim secara otomatis dengan permintaan. Namun, jika Anda menggunakan cookie untuk autentikasi, perlindungan CSRF sangat penting karena cookie dikirim secara otomatis. Terlepas dari metode autentikasi yang Anda gunakan, selalu terapkan langkah-langkah keamanan yang tepat seperti HTTPS, validasi input, dan pembatasan jumlah permintaan.
Pemecahan Masalah
Token Tidak Tersimpan Setelah Halaman Disegarkan
Masalah: Setelah masuk, menyegarkan halaman membuat Anda keluar.
Solusi:
- Verifikasi token disimpan di
localStorageatausessionStorage(periksa DevTools browser > Application > Storage) - Pastikan
getToken()dijalankan saat komponen dimuat dan menginisialisasi state dengan benar - Periksa kesalahan di konsol browser yang mungkin menghalangi pengambilan token
- Pastikan format token sesuai dengan yang diharapkan oleh kode Anda (objek dengan properti
token)
Kesalahan “Tidak dapat membaca properti ‘token’ dari null”
Masalah: Terjadi kesalahan saat mengakses userToken.token setelah JSON.parse().
Solusi: Tambahkan pengecekan null sebelum melakukan parsing:
const tokenString = localStorage.getItem('token');
if (!tokenString) return null;
const userToken = JSON.parse(tokenString);
return userToken?.token;Formulir Masuk Tidak Terkirim
Masalah: Klik tombol kirim tidak memicu permintaan login.
Solusi:
- Pastikan formulir memiliki
onSubmit={handleSubmit}dan tombol memilikitype="submit" - Periksa apakah server API berjalan pada port yang benar (default: 8080)
- Periksa apakah CORS diaktifkan di server jika melakukan permintaan lintas asal
- Periksa konsol browser untuk kesalahan jaringan atau masalah CORS
Rute Dilindungi Tidak Berfungsi
Masalah: Pengguna dapat mengakses rute yang dilindungi tanpa autentikasi.
Solusi:
- Verifikasi bahwa komponen
ProtectedRoutememeriksa token sebelum dirender - Pastikan token diteruskan ke
ProtectedRouteatau diakses melalui Context - Periksa apakah rute dibungkus dengan benar di
App.js - Verifikasi sintaks React Router v6 (prop
element, bukan children)
Token Kedaluwarsa Saat Pengguna Sedang Aktif
Masalah: Pengguna keluar secara tak terduga saat sedang aktif menggunakan.
Solusi:
- Implementasikan logika penyegaran token menggunakan token penyegar
- Periksa masa berlaku token sebelum melakukan panggilan API
- Tambahkan pembaruan token otomatis saat masa berlaku hampir habis
- Pertimbangkan untuk memperpanjang masa berlaku token bagi pengguna aktif
localStorage Dinonaktifkan atau Kuota Terlampaui
Masalah: Token tidak tersimpan dalam mode pribadi/incognito atau kuota penyimpanan penuh.
Solusi:
- Periksa apakah
localStoragetersedia:if (typeof(Storage) !== "undefined") - Tangani
QuotaExceededErrorsaat mengatur item - Kembali menggunakan
sessionStorageatau penyimpanan di memori - Bersihkan token lama sebelum menyimpan yang baru
- Beri tahu pengguna jika penyimpanan tidak tersedia
Beberapa Tab Tidak Menyinkronkan Status Otentikasi
Masalah: Keluar di satu tab tidak membuat keluar di tab lainnya.
Solusi: Dengarkan peristiwa storage untuk menyinkronkan status antar tab:
useEffect(() => {
const handleStorageChange = (e) => {
if (e.key === 'token') {
// Update auth state when token changes in another tab
setToken(e.newValue ? JSON.parse(e.newValue) : null);
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, []);API Mengembalikan Format Token yang Berbeda
Masalah: API Anda mengembalikan { accessToken: ''...'' } alih-alih { token: ''...'' }.
Solusi: Perbarui kode agar sesuai dengan format respons API Anda:
// If API returns { accessToken: '...' }
const token = userToken?.accessToken;
// Or if API returns { data: { token: '...' } }
const token = userToken?.data?.token;Kesalahan 401 Tidak Sah
Masalah: API mengembalikan 401 meskipun dengan token yang valid.
Solusi:
- Pastikan token dikirim di header request:
Authorization: Bearer $ {token} - Periksa masa berlaku token (decode JWT untuk melihat klaim
exp) - Pastikan format token sesuai dengan yang diharapkan server
- Verifikasi server memvalidasi token dengan benar
- Periksa apakah token perlu diperbarui
Pertanyaan yang Sering Diajukan
1. Bagaimana cara melindungi rute di React?
Gunakan komponen pembungkus ProtectedRoute yang memeriksa autentikasi sebelum merender. Dalam React Router v6, bungkus rute yang dilindungi dengan komponen yang memverifikasi keberadaan token. Jika token tidak ditemukan, alihkan ke halaman login menggunakan Navigate:
function ProtectedRoute({ children, token }) {
if (!token) {
return <Navigate to="/login" replace />;
}
return children;
}Lalu gunakan itu di rute Anda:
<Route
path="/dashboard"
element={
<ProtectedRoute token={token}>
<Dashboard />
</ProtectedRoute>
}
/>2. Di mana sebaiknya saya menyimpan token otentikasi?
Simpan token di localStorage untuk bertahan antar sesi, atau sessionStorage untuk sesi spesifik tab. Keduanya dapat diakses oleh JavaScript, sehingga rentan terhadap serangan XSS, jadi selalu sanitasi input pengguna dan gunakan HTTPS.
localStorage: Bertahan di seluruh sesi dan tab browser. Gunakan untuk fungsi “ingat saya”.sessionStorage: Dihapus saat tab ditutup. Gunakan untuk otentikasi yang lebih aman, hanya selama sesi.- Cookies: Secara otomatis dikirim dengan permintaan, memerlukan perlindungan CSRF. Gunakan dengan flag
httpOnlyuntuk akses hanya di sisi server.
Untuk aplikasi produksi, pertimbangkan menggunakan cookie yang aman dan httpOnly yang dikelola oleh backend Anda, yang lebih tahan terhadap serangan XSS.
3. Apakah localStorage aman untuk otentikasi?
localStorage rentan terhadap serangan XSS tetapi dapat diterima untuk banyak aplikasi jika dikombinasikan dengan langkah-langkah keamanan yang tepat. Setiap JavaScript yang berjalan di domain Anda dapat mengakses localStorage, jadi jika seorang penyerang menyuntikkan skrip berbahaya, mereka dapat mencuri token.
Strategi mitigasi:
- Bersihkan semua input pengguna untuk mencegah XSS
- Gunakan header Content Security Policy (CSP)
- Terapkan kadaluwarsa token dan perbarui token
- Gunakan HTTPS untuk mencegah serangan man-in-the-middle
- Pertimbangkan cookie httpOnly untuk aplikasi yang sensitif
Untuk aplikasi dengan keamanan tinggi (perbankan, layanan kesehatan), sebaiknya gunakan cookie httpOnly dengan perlindungan CSRF, karena cookie ini tidak dapat diakses oleh JavaScript.
4. Apakah aplikasi React memerlukan OAuth?
OAuth diperlukan saat melakukan autentikasi dengan penyedia pihak ketiga (Google, GitHub, Facebook) tetapi tidak untuk autentikasi kustom. Jika Anda membuat sistem autentikasi sendiri dengan nama pengguna/kata sandi, Anda dapat menggunakan autentikasi berbasis token seperti yang ditunjukkan dalam tutorial ini.
Gunakan OAuth ketika:
- Pengguna harus masuk dengan akun sosial
- Anda ingin mendelegasikan autentikasi ke penyedia yang terpercaya
- Anda perlu mengakses API pihak ketiga atas nama pengguna
Gunakan otentikasi khusus ketika:
- Anda mengontrol basis data pengguna
- Anda memerlukan kendali penuh atas alur otentikasi
- Anda ingin menghindari ketergantungan pihak ketiga
Banyak aplikasi menggunakan pendekatan hybrid: OAuth untuk login sosial dan autentikasi kustom untuk email/sandi.
5. Bagaimana saya menangani kedaluwarsa token?
Decode token JWT di sisi klien untuk memeriksa masa kedaluwarsa, kemudian perbarui atau keluar dari pengguna. Gunakan library seperti jwt-decode untuk membaca klaim exp:
import { jwtDecode } from 'jwt-decode';
const decoded = jwtDecode(token);
const currentTime = Date.now() / 1000;
if (decoded.exp < currentTime) {
// Token expired, remove it and redirect to login
localStorage.removeItem('token');
navigate('/login');
}Untuk produksi, terapkan token penyegaran: token akses yang berlaku singkat (15-30 menit) dan token penyegaran yang berlaku lebih lama (7-30 hari) yang dapat digunakan untuk mendapatkan token akses baru tanpa harus autentikasi ulang.
6. Bagaimana cara saya mengimplementasikan fungsi "Ingat Saya"?
Simpan token di localStorage untuk sesi yang persisten dan di sessionStorage untuk sesi sementara. Tambahkan kotak centang ke formulir login Anda:
const [rememberMe, setRememberMe] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
const token = await loginUser(credentials);
if (rememberMe) {
localStorage.setItem('token', JSON.stringify(token));
} else {
sessionStorage.setItem('token', JSON.stringify(token));
}
};Saat rememberMe dicentang, token tetap ada antar sesi browser. Jika tidak, token akan dihapus saat tab ditutup.
Kesimpulan
Otentikasi dalam aplikasi React memerlukan keseimbangan antara keamanan, pengalaman pengguna, dan kemudahan pemeliharaan. Dengan memusatkan logika otentikasi dalam custom hooks atau React Context, menerapkan rute terlindungi dengan React Router v6, dan memilih strategi penyimpanan token yang tepat, Anda dapat membangun alur otentikasi yang aman dan ramah pengguna.
Prinsip utama yang perlu diingat:
- Simpan token di penyimpanan browser (
localStorageatausessionStorage) agar tetap ada saat halaman dimuat ulang - Gunakan komponen rute yang dilindungi untuk menegakkan persyaratan otentikasi
- Dekode token JWT di sisi klien untuk memeriksa kedaluwarsa, tetapi selalu validasi di server
- Terapkan penanganan kesalahan dan status pemuatan untuk pengalaman pengguna yang lebih baik
- Pertimbangkan perlindungan CSRF saat menggunakan autentikasi berbasis cookie
- Gunakan React Context untuk status autentikasi di aplikasi yang lebih besar agar menghindari prop drilling
Setiap solusi penyimpanan dan pendekatan autentikasi memiliki kelebihan dan kekurangannya. localStorage memberikan kemudahan tetapi memerlukan perlindungan XSS. sessionStorage lebih aman tetapi kurang nyaman. Cookie memerlukan perlindungan CSRF tetapi kurang rentan terhadap XSS. Pilih berdasarkan kebutuhan keamanan dan tujuan pengalaman pengguna Anda.
Seiring pertumbuhan aplikasi Anda, pertimbangkan untuk menerapkan token penyegaran, autentikasi multi-faktor, dan fitur manajemen sesi. Untuk aplikasi produksi, selalu gunakan HTTPS, terapkan pembatasan laju, dan ikuti praktik keamanan terbaik.
Jika Anda ingin membaca lebih banyak tutorial React, kunjungi halaman Topik React kami, atau jelajahi tutorial terkait seperti Cara Membangun Aplikasi To-Do Menggunakan Django dan React serta Cara Menerapkan Aplikasi React dengan Nginx di Ubuntu.
Terima kasih telah belajar bersama Komunitas Linux-Console.net.








