Πώς να Προσθέσετε Έλεγχο Ταυτότητας Σύνδεσης σε Εφαρμογές React
Ο συγγραφέας επέλεξε τα Creative Commons για να λάβει μια δωρεά στο πλαίσιο του προγράμματος Write for Donations.
Εισαγωγή
Η αυθεντικοποίηση στις εφαρμογές React διαχειρίζεται την πρόσβαση των χρηστών σε προστατευμένες σελίδες και πόρους. Όταν ένας χρήστης συνδέεται, η εφαρμογή σας αποθηκεύει τα διαπιστευτήρια αυθεντικοποίησης (συνήθως ένα token) και τα ελέγχει πριν αποδώσει τις ιδιωτικές διαδρομές. Η κατάσταση αυθεντικοποίησης πρέπει να διατηρείται κατά τις ανανεώσεις σελίδας, κάτι που απαιτεί την αποθήκευση των token στην αποθήκευση του προγράμματος περιήγησης αντί στην κατάσταση του component.
Πώς διατηρείται η κατάσταση ταυτοποίησης: Όταν αποθηκεύετε tokens στο localStorage ή στο sessionStorage, αυτά επιβιώνουν από ανανεώσεις σελίδας και επανεκκινήσεις του προγράμματος περιήγησης (για το localStorage). Αυτό σημαίνει ότι η εφαρμογή σας μπορεί να ελέγξει για υπάρχον token κατά την εκκίνηση και να αυθεντικοποιήσει αυτόματα τους χρήστες χωρίς να απαιτείται νέα σύνδεση. Χωρίς μόνιμη αποθήκευση, οι χρήστες θα χρειάζονταν να συνδέονται κάθε φορά που ανανεώνουν τη σελίδα ή ανοίγουν μια νέα καρτέλα.
Αυτό το σεμινάριο εξηγεί πώς να δημιουργήσετε μια εφαρμογή React με έλεγχο ταυτότητας βασισμένο σε token χρησιμοποιώντας το React Router v6 για προστατευμένες διαδρομές. Θα δημιουργήσετε ένα mock API που επιστρέφει tokens ελέγχου ταυτότητας, θα υλοποιήσετε μια σελίδα σύνδεσης που θα αντλεί και θα αποθηκεύει τα tokens, και θα ρυθμίσετε ελέγχους ταυτότητας που διατηρούν την επιθυμητή διαδρομή του χρήστη. Το σεμινάριο καλύπτει διαφορετικές μεθόδους αποθήκευσης tokens (localStorage, sessionStorage) και τις επιπτώσεις τους στην ασφάλεια, υλοποιεί λειτουργία αποσύνδεσης και διαχείριση σφαλμάτων, και δείχνει πώς να αποκωδικοποιείτε JWT tokens για να ελέγξετε τη λήξη τους.
Γιατί η λογική αυθεντικοποίησης δεν πρέπει να βρίσκεται σε μεμονωμένα components: Η συγκέντρωση της λογικής αυθεντικοποίησης σε ένα προσαρμοσμένο hook ή στο React Context εξασφαλίζει συνεπή συμπεριφορά σε όλη την εφαρμογή σας, διευκολύνει την ενημέρωση των ροών αυθεντικοποίησης και αποτρέπει την επανάληψη κώδικα. Όταν η λογική αυθεντικοποίησης είναι διασκορπισμένη σε διάφορα components, υπάρχει κίνδυνος ασυνεπών ελέγχων token, παραλείψεων σε ακραίες περιπτώσεις και δυσκολιών στη συντήρηση.
Μετά την ολοκλήρωση αυτού του σεμιναρίου, θα κατανοείτε πώς να υλοποιήσετε ασφαλή έλεγχο ταυτότητας σε εφαρμογές React, να επιλέγετε κατάλληλες στρατηγικές αποθήκευσης διακριτικών με βάση τις απαιτήσεις ασφάλειας και εμπειρίας χρήστη, και να χειρίζεστε κοινά σενάρια ελέγχου ταυτότητας, συμπεριλαμβανομένων προστατευόμενων διαδρομών, λήξης διακριτικών και αποσύνδεσης.
Χρειάζεστε να αναπτύξετε ένα έργο React γρήγορα; Δείτε την πλατφόρμα εφαρμογών Linux-Console.net και αναπτύξτε ένα έργο React απευθείας από το GitHub σε λίγα λεπτά.
Κύρια Συμπεράσματα
Η κατάσταση πιστοποίησης παραμένει ακόμη και μετά την ανανέωση της σελίδας όταν αποθηκεύεται στο
localStorageή στοsessionStorage. Τα tokens που αποθηκεύονται στην κατάσταση του component (useState) χάνονται κατά την ανανέωση, απαιτώντας από τους χρήστες να συνδεθούν ξανά. Τα APIs αποθήκευσης του προγράμματος περιήγησης διατηρούν τα tokens μεταξύ των συνεδριών, επιτρέποντας την αυτόματη επαλήθευση ταυτότητας.Το React Router v6 χρησιμοποιεί
Routesαντί γιαSwitchκαι απαιτεί ιδιότητεςelementαντί για children. Το νέο API είναι πιο δηλωτικό και ασφαλές ως προς τους τύπους. Οι προστατευμένες διαδρομές υλοποιούνται χρησιμοποιώντας ένα component-περιτύλιγμα που ελέγχει την ταυτότητα πριν την απόδοση.Κεντράρετε τη λογική αυθεντικοποίησης σε ένα προσαρμοσμένο hook ή React Context. Η διατήρηση της λογικής αυθεντικοποίησης σε μεμονωμένα components οδηγεί σε διπλοεγγραφές και ασυνεπή συμπεριφορά. Ένα προσαρμοσμένο
useTokenhook ήAuthContextπαρέχει μια μοναδική πηγή αλήθειας για την κατάσταση αυθεντικοποίησης.localStorageδιατηρείται μεταξύ των συνεδριών του browser·sessionStorageείναι συγκεκριμένο για κάθε καρτέλα. ΧρησιμοποιήστεlocalStorageγια λειτουργία «θυμήσου με» καιsessionStorageγια πιο ασφαλή, μόνο-συνεδρία αυθεντικοποίηση. Και τα δύο είναι ευάλωτα σε επιθέσεις XSS, οπότε πάντα καθαρίζετε τα δεδομένα που εισάγει ο χρήστης.Τα JWT tokens μπορούν να αποκωδικοποιηθούν στον πελάτη για να ελεγχθεί η λήξη τους χωρίς να χρειάζεται γύρος στον διακομιστή. Το payload περιέχει ένα
expclaim (χρόνος Unix) που μπορείτε να συγκρίνετε με τον τρέχοντα χρόνο. Ωστόσο, πάντα να επικυρώνετε τα tokens στον διακομιστή καθώς οι έλεγχοι από την πλευρά του πελάτη είναι μόνο για την εμπειρία χρήστη (UX).Η προστασία CSRF είναι κρίσιμη κατά τη χρήση πιστοποίησης βασισμένης σε cookies. Τα cookies αποστέλλονται αυτόματα με τα αιτήματα, καθιστώντας τα ευάλωτα σε πλαστογράφηση αιτήσεων από διαφορετικούς ιστότοπους. Χρησιμοποιήστε tokens CSRF, ιδιότητες cookie SameSite και ελέγξτε την κεφαλίδα Origin στον διακομιστή.
Προαπαιτούμενα
Ένα περιβάλλον ανάπτυξης που τρέχει Node.js έκδοσης 18.x και npm έκδοσης 9.x. Για να εγκαταστήσετε το Node.js σε macOS ή Ubuntu, ακολουθήστε τα βήματα στο Πώς να Εγκαταστήσετε το Node.js και να Δημιουργήσετε ένα Τοπικό Περιβάλλον Ανάπτυξης σε macOS ή Πώς να Εγκαταστήσετε το Node.js σε Ubuntu.
-
Ένα περιβάλλον ανάπτυξης React ρυθμισμένο με το Create React App, με την μη απαραίτητη βασική δομή αφαιρεμένη. Για να το ρυθμίσετε, ακολουθήστε το Βήμα 1 — Δημιουργία Κενής Εφαρμογής από το σεμινάριο Πώς να Διαχειριστείτε Κατάσταση σε Component Τάξης React. Αυτό το σεμινάριο χρησιμοποιεί το
auth-tutorialως όνομα έργου. Εξοικείωση με την ανάκτηση δεδομένων από APIs στο React. Για περισσότερα σχετικά με την εργασία με APIs, δείτε Πώς να Καλέσετε Web APIs με το useEffect Hook στο React.
Βασικές γνώσεις JavaScript, HTML και CSS. Μάθετε περισσότερα στη σειρά μας Πώς να Δημιουργήσετε μια Ιστοσελίδα με HTML, Πώς να Στυλιζάρετε HTML με CSS και Πώς να Κωδικοποιείτε σε JavaScript.
Βήμα 1 — Δημιουργία σελίδας σύνδεσης
Οι σελίδες σύνδεσης παρεμβαίνουν στους μη αυθεντικοποιημένους χρήστες πριν φτάσουν σε προστατευμένες διαδρομές. Αντί να ανακατευθύνετε τους χρήστες σε μια αφιερωμένη σελίδα σύνδεσης (η οποία χάνει τον προορισμό που επιθυμούσαν), εμφανίστε τη φόρμα σύνδεσης σε οποιαδήποτε διαδρομή όταν δεν υπάρχει διακριτικό αυθεντικοποίησης. Αυτό διατηρεί την πρόθεση του χρήστη να επισκεφθεί το /dashboard ενώ είναι μη συνδεδεμένος, δείχνοντας τη φόρμα σύνδεσης, και μετά την αυθεντικοποίηση, θα προσγειωθεί στο /dashboard αντί να ανακατευθυνθεί σε άλλο σημείο.
Γιατί να εμφανίζεται η φόρμα σύνδεσης σε οποιαδήποτε διαδρομή: Όταν οι χρήστες επισκέπτονται μια προστατευμένη διαδρομή όπως το /dashboard ενώ δεν είναι αυθεντικοποιημένοι, η εμφάνιση της φόρμας σύνδεσης στην ίδια διαδρομή διατηρεί το πλαίσιο πλοήγησής τους. Μετά τη σύνδεση, βρίσκονται ήδη στον προορισμό που επιθυμούσαν, χωρίς επιπλέον ανακατευθύνσεις. Αυτό δημιουργεί μια πιο ομαλή εμπειρία χρήστη σε σύγκριση με την ανακατεύθυνση στο /login και την ανάγκη για ένα ακόμα βήμα πλοήγησης.
Εγκαταστήστε το React Router χρησιμοποιώντας το npm. Υπάρχουν δύο εκδόσεις: μια έκδοση για web για εφαρμογές React και μια εγγενής έκδοση για React Native. Εγκαταστήστε την έκδοση για web:
npm install react-router-domΗ εγκατάσταση ολοκληρώνεται με έξοδο παρόμοια με:
...
+ react-router-dom@6.28.0
added 11 packages from 6 contributors, removed 10 packages and audited 1945 packages in 12.794s
...Στη συνέχεια, δημιουργήστε δύο στοιχεία που ονομάζονται Dashboard και Preferences για να λειτουργήσουν ως ιδιωτικές σελίδες. Αυτά θα αντιπροσωπεύουν στοιχεία που ένας χρήστης δεν θα πρέπει να δει μέχρι να έχει συνδεθεί επιτυχώς στην εφαρμογή.
Πρώτα, δημιουργήστε τους φακέλους:
mkdir src/components/Dashboard
mkdir src/components/PreferencesΑνοίξτε το Dashboard.js σε έναν επεξεργαστή κειμένου:
nano src/components/Dashboard/Dashboard.js
Μέσα στο Dashboard.js, πρόσθεσε μια ετικέτα <h2> με το περιεχόμενο Dashboard:
import React from 'react';
export default function Dashboard() {
return(
<h2>Dashboard</h2>
);
}Αποθήκευσε και κλείσε το αρχείο.
Επαναλάβετε τα ίδια βήματα για τις Προτιμήσεις. Ανοίξτε το στοιχείο:
nano src/components/Preferences/Preferences.jsΠρόσθεσε το περιεχόμενο:
import React from 'react';
export default function Preferences() {
return(
<h2>Preferences</h2>
);
}Αποθήκευσε και κλείσε το αρχείο.
Τώρα που έχετε μερικά components, πρέπει να εισαγάγετε τα components και να δημιουργήσετε routes μέσα στο App.js. Δείτε το σεμινάριο How To Handle Routing in React Apps with React Router για μια πλήρη εισαγωγή στο routing σε εφαρμογές React. Για τη φιλοξενία της εφαρμογής σας React, δείτε το How To Deploy a React Application with Nginx on Ubuntu.
Για να ξεκινήσετε, ανοίξτε το App.js:
nano src/components/App/App.jsΣτη συνέχεια, εισάγετε το Dashboard και το Preferences προσθέτοντας τον παρακάτω επισημασμένο κώδικα:
import React from 'react';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';
function App() {
return (
<></>
);
}
export default App;Στη συνέχεια, εισάγετε το BrowserRouter, το Routes και το Route από το react-router-dom. Το React Router v6 χρησιμοποιεί το Routes αντί για το Switch, και οι διαδρομές χρησιμοποιούν το prop element αντί για 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;Προσθέστε ένα περιβάλλον <div> με className ως wrapper και μια ετικέτα <h1> για να χρησιμεύσει ως πρότυπο για την εφαρμογή. Βεβαιωθείτε ότι εισάγετε το App.css ώστε να μπορείτε να εφαρμόσετε τα στυλ.
Στη συνέχεια, δημιουργήστε διαδρομές για τα στοιχεία Dashboard και Preferences. Προσθέστε το BrowserRouter, στη συνέχεια προσθέστε ένα στοιχείο Routes ως παιδί. Μέσα στα Routes, προσθέστε ένα Route με ιδιότητες path και element για κάθε στοιχείο:
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;Αποθήκευσε και κλείσε το αρχείο.
Το τελευταίο βήμα είναι να προσθέσετε λίγο padding στο κύριο <div> ώστε το στοιχείο σας να μην βρίσκεται απευθείας στην άκρη του περιηγητή. Για να το κάνετε αυτό, θα αλλάξετε το CSS.
Άνοιξε το App.css:
nano src/components/App/App.cssΑντικαταστήστε το περιεχόμενο με μια κλάση .wrapper με padding 20px:
.wrapper {
padding: 20px;
}Αποθήκευσε και κλείσε το αρχείο. Ο περιηγητής φορτώνει ξανά αυτόματα, εμφανίζοντας τα βασικά στοιχεία:
Επισκεφθείτε http://localhost:3000/dashboard για να δείτε τη σελίδα του πίνακα ελέγχου:
Οι διαδρομές λειτουργούν, αλλά οι /dashboard και /preferences είναι επί του παρόντος προσβάσιμες σε οποιονδήποτε, συμπεριλαμβανομένων των μη αυθεντικοποιημένων χρηστών. Αυτές θα πρέπει να είναι προστατευμένες διαδρομές.
Δύο προσεγγίσεις για την προστασία διαδρομών:
Ανακατεύθυνση σε ειδική διαδρομή σύνδεσης: Δημιουργήστε μια διαδρομή
/loginκαι ανακατευθύνετε εκεί τους μη αυθεντικοποιημένους χρήστες. Αυτό λειτουργεί, αλλά οι χρήστες χάνουν τον επιθυμητό προορισμό τους και πρέπει να επιστρέψουν χειροκίνητα μετά τη σύνδεση.Εμφάνιση φόρμας σύνδεσης σε οποιαδήποτε διαδρομή (χρησιμοποιείται σε αυτό το σεμινάριο): Εμφανίστε τη φόρμα σύνδεσης στην τρέχουσα διαδρομή όταν δεν υπάρχει token. Μετά την πιστοποίηση, οι χρήστες παραμένουν στον προορισμό που επιθυμούν. Για παράδειγμα, επισκεπτόμενοι το
/dashboardενώ δεν είστε συνδεδεμένοι, εμφανίζεται η φόρμα σύνδεσης και μετά τη σύνδεση, οι χρήστες μεταβαίνουν απευθείας στο/dashboard.
Αυτό το σεμινάριο χρησιμοποιεί τη δεύτερη προσέγγιση για καλύτερη εμπειρία χρήστη.
Για να ξεκινήσετε, δημιουργήστε έναν νέο φάκελο για το στοιχείο Login:
mkdir src/components/LoginΣτη συνέχεια, ανοίξτε το Login.js σε έναν επεξεργαστή κειμένου:
nano src/components/Login/Login.jsΔημιουργήστε μια βασική φόρμα με ένα κουμπί υποβολής <button> και ένα <input> για το όνομα χρήστη και τον κωδικό πρόσβασης. Βεβαιωθείτε ότι έχετε ορίσει τον τύπο εισόδου για τον κωδικό πρόσβασης σε 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>
)
}Για περισσότερα σχετικά με τις φόρμες στο React, δείτε τον οδηγό Πώς να Δημιουργήσετε Φόρμες στο React. Για να μάθετε πώς να διαχειρίζεστε την κατάσταση στα στοιχεία του React, δείτε Πώς να Διαχειρίζεστε την Κατάσταση με τα Hooks στα Στοιχεία του React.
Στη συνέχεια, προσθέστε μια ετικέτα <h1> που ζητά από τον χρήστη να συνδεθεί. Περιβάλλετε τη <form> και την <h1> με ένα <div> που έχει className login-wrapper. Τέλος, εισάγετε το 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>
)
}Αποθήκευσε και κλείσε το αρχείο.
Προσθέστε στυλ για να κεντράρετε τη φόρμα εισόδου. Ανοίξτε το Login.css:
nano src/components/Login/Login.cssΚεντράρετε το στοιχείο στη σελίδα προσθέτοντας ένα display με τιμή flex, στη συνέχεια ορίζοντας το flex-direction σε column για να στοιχίσετε τα στοιχεία κάθετα και προσθέτοντας align-items με τιμή center για να κάνετε το στοιχείο κεντραρισμένο στο πρόγραμμα περιήγησης:
.login-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}Για περισσότερες πληροφορίες σχετικά με τη χρήση του Flexbox, δείτε τον Οδηγό Απαραβίαστων CSS Flexbox μας
Αποθήκευσε και κλείσε το αρχείο.
Απεικονίστε το στοιχείο σύνδεσης στο App.js όταν δεν υπάρχει διακριτικό αυθεντικοποίησης. Ανοίξτε το App.js:
nano src/components/App/App.jsΣτο Βήμα 3, θα υλοποιήσετε μόνιμη αποθήκευση των tokens. Προς το παρόν, αποθηκεύστε το token στην κατάσταση του component χρησιμοποιώντας το useState Hook για να δοκιμάσετε τη ροή σύνδεσης. Σημειώστε ότι τα tokens που αποθηκεύονται στην κατάσταση του component χάνονται όταν ανανεώνεται η σελίδα.
Εισάγετε το useState από το react, στη συνέχεια καλέστε το useState και ορίστε τις επιστρεφόμενες τιμές σε token και 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;Εισάγετε το στοιχείο Login. Προσθέστε μια συνθήκη για να εμφανίζεται το Login αν το token είναι ψευδές.
Περνάτε τη συνάρτηση setToken στο στοιχείο 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;Δεδομένου ότι δεν υπάρχει ακόμα κάποιο διακριτικό, η φόρμα σύνδεσης εμφανίζεται σε όλες τις διαδρομές. Στο επόμενο βήμα, θα δημιουργήσετε ένα API endpoint που επιστρέφει ένα διακριτικό μετά από επιτυχή έλεγχο ταυτότητας.
Αποθηκεύστε και κλείστε το αρχείο. Ο περιηγητής φορτώνει ξανά αυτόματα, εμφανίζοντας τη σελίδα σύνδεσης σε όλες τις διαδρομές. Επισκεφθείτε τη σελίδα του πίνακα ελέγχου για να επιβεβαιώσετε ότι εμφανίζεται η φόρμα σύνδεσης:
Έχετε δημιουργήσει μια φόρμα σύνδεσης που εμφανίζεται σε όλες τις διαδρομές όταν δεν υπάρχει διακριτικό αυθεντικοποίησης. Η φόρμα εμποδίζει την πρόσβαση σε προστατευμένες διαδρομές όπως /dashboard και /preferences έως ότου η αυθεντικοποίηση ολοκληρωθεί με επιτυχία. Στο επόμενο βήμα, θα δημιουργήσετε έναν δοκιμαστικό διακομιστή API που επιστρέφει διακριτικά αυθεντικοποίησης.
Βήμα 2 — Δημιουργία ενός Token API
Τα Mock APIs προσομοιώνουν την πιστοποίηση στο backend κατά την ανάπτυξη χωρίς να απαιτείται διακομιστής παραγωγής. Το Express παρέχει έναν απλό τρόπο για να δημιουργήσετε έναν διακομιστή ανάπτυξης που επιστρέφει tokens στο ίδιο μορφότυπο που θα χρησιμοποιεί το API παραγωγής σας. Αυτό σας επιτρέπει να δοκιμάσετε ροές πιστοποίησης, να χειριστείτε τις απαντήσεις του API και να αναπτύξετε το frontend ανεξάρτητα.
Γιατί να χρησιμοποιήσετε ένα mock API: Κατά την ανάπτυξη, μπορεί να μην έχετε έτοιμο backend, ή θέλετε να δοκιμάσετε ροές αυθεντικοποίησης χωρίς να συνδεθείτε σε έναν παραγωγικό διακομιστή. Ένα mock API επιστρέφει προβλέψιμες απαντήσεις, καθιστώντας πιο εύκολη τη δοκιμή της διαχείρισης σφαλμάτων, των καταστάσεων φόρτωσης και της διαχείρισης διακριτικών. Για εφαρμογές παραγωγής, αντικαταστήστε το mock API με το πραγματικό endpoint αυθεντικοποίησής σας.
Δημιουργήστε έναν απλό διακομιστή Express που επιστρέφει ένα διακριτικό (token). Για μια αναλυτική εισαγωγή στο Express, δείτε το Βασικό Διακομιστή Express στο Node.js. Αν δημιουργείτε μια πλήρη εφαρμογή, εξετάστε το Πώς να Δημιουργήσετε μια Εφαρμογή To-Do Χρησιμοποιώντας Django και React για ένα πλήρες παράδειγμα.
Για να ξεκινήσετε, εγκαταστήστε το express. Δεδομένου ότι ο διακομιστής δεν είναι απαραίτητος για την τελική κατασκευή, φροντίστε να τον εγκαταστήσετε ως devDependency.
Θα χρειαστεί επίσης να εγκαταστήσετε το cors. Αυτή η βιβλιοθήκη θα επιτρέψει την κοινή χρήση πόρων μεταξύ διαφορετικών προελεύσεων για όλες τις διαδρομές.
Προειδοποίηση: Μην ενεργοποιείτε το CORS για όλες τις διαδρομές σε μια εφαρμογή παραγωγής. Αυτό μπορεί να οδηγήσει σε ζητήματα ασφάλειας.
npm install --save-dev express corsΗ εγκατάσταση ολοκληρώνεται με έξοδο παρόμοια με:
...
+ cors@2.8.5
+ express@4.17.1
removed 10 packages, updated 2 packages and audited 2059 packages in 12.597s
...Στη συνέχεια, ανοίξτε ένα νέο αρχείο με όνομα server.js στη ρίζα της εφαρμογής σας. Μην προσθέσετε αυτό το αρχείο στον κατάλογο /src, καθώς δεν θέλετε να αποτελεί μέρος του τελικού build.
nano server.jsΕισάγετε το express, στη συνέχεια αρχικοποιήστε μια νέα εφαρμογή καλώντας το express() και αποθηκεύοντας το αποτέλεσμα σε μια μεταβλητή που ονομάζεται app:
const express = require('express');
const app = express();Μετά τη δημιουργία της app, προσθέστε το cors ως middleware. Πρώτα, εισάγετε το cors και μετά προσθέστε το στην εφαρμογή καλώντας τη μέθοδο use στο app:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());Προσθέστε έναν χειριστή διαδρομής για το /login που επιστρέφει ένα διακριτικό (token). Ο χειριστής λαμβάνει req (δεδομένα αιτήματος) και res (αντικείμενο απόκρισης). Χρησιμοποιήστε res.send για να επιστρέψετε ένα αντικείμενο JavaScript που περιέχει ένα διακριτικό:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.use('/login', (req, res) => {
res.send({
token: 'test123'
});
});Τέλος, εκκινήστε τον διακομιστή στη θύρα 8080 χρησιμοποιώντας 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'));Αποθήκευσε και κλείσε το αρχείο. Σε ένα νέο παράθυρο ή καρτέλα τερματικού, εκκίνησε τον διακομιστή:
node server.jsΟ διακομιστής ξεκινά και εμφανίζει:
API is running on http://localhost:8080/loginΕπισκεφθείτε τη σελίδα σύνδεσης στον περιηγητή σας για να δείτε την απόκριση του αντικειμένου JSON:
Σημείωση: Η επίσκεψη της διεύθυνσης URL σε ένα πρόγραμμα περιήγησης στέλνει ένα αίτημα GET, ενώ η φόρμα σύνδεσης θα στείλει ένα αίτημα POST. Ο τρέχων χειριστής διαδρομής δέχεται και τις δύο μεθόδους. Σε παραγωγικό περιβάλλον, περιορίστε τις διαδρομές σε συγκεκριμένες μεθόδους HTTP χρησιμοποιώντας app.post() για αιτήματα POST και app.get() για αιτήματα GET. Δείτε την τεκμηρίωση δρομολόγησης του Express για λεπτομέρειες.
Τώρα που έχετε έναν λειτουργικό διακομιστή API, χρειάζεται να κάνετε ένα αίτημα από τη σελίδα σύνδεσής σας. Ανοίξτε το Login.js:
nano src/components/Login/Login.jsΣτο προηγούμενο βήμα, περάσατε ένα νέο prop με όνομα setToken στο component Login. Προσθέστε το PropType για το νέο prop και κάντε αποδόμηση (destructure) του αντικειμένου props για να εξάγετε το 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
}Στη συνέχεια, δημιούργησε ένα τοπικό state για να καταγράψεις το Username και το Password. Δεδομένου ότι δεν χρειάζεται να ορίσεις τα δεδομένα χειροκίνητα, κάνε τα <inputs> ανεξέλεγκτα components. Μπορείς να βρεις αναλυτικές πληροφορίες για τα ανεξέλεγκτα components στο How To Build Forms in 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
};Δημιουργήστε μια συνάρτηση για να κάνετε ένα POST αίτημα στον διακομιστή. Σε μεγαλύτερες εφαρμογές, τοποθετήστε τις κλήσεις API σε έναν ξεχωριστό φάκελο υπηρεσιών. Για αυτό το σεμινάριο, η συνάρτηση ορίζεται στο αρχείο του component. Για περισσότερα σχετικά με τις κλήσεις API στο React, δείτε το How To Call Web APIs with the useEffect Hook in React.
Δημιουργήστε μια async συνάρτηση που ονομάζεται loginUser. Η συνάρτηση θα παίρνει τα credentials ως όρισμα και στη συνέχεια θα καλεί τη μέθοδο fetch χρησιμοποιώντας την επιλογή 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 }) {
...Τέλος, δημιουργήστε έναν χειριστή υποβολής φόρμας που ονομάζεται handleSubmit που θα καλέσει το loginUser με το username και το password. Καλέστε το setToken με ένα επιτυχημένο αποτέλεσμα. Καλέστε το handleSubmit χρησιμοποιώντας τον χειριστή γεγονότων onSubmit στη <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
};Σημείωση: Σε εφαρμογές παραγωγής, χειριστείτε την αποσύνδεση των στοιχείων κατά τη διάρκεια ασύγχρονων λειτουργιών. Αν ο χρήστης μεταβεί σε άλλη σελίδα πριν επιλυθεί μια Promise, ίσως χρειαστεί να ακυρώσετε το αίτημα ή να αγνοήσετε την απάντηση. Δείτε Πώς να Καλέσετε Web APIs με το useEffect Hook στο React για προτυπά καθαρισμού.
Αποθηκεύστε και κλείστε το αρχείο. Βεβαιωθείτε ότι ο διακομιστής API εξακολουθεί να λειτουργεί, στη συνέχεια επισκεφθείτε το http://localhost:3000/dashboard στον περιηγητή σας.
Η σελίδα σύνδεσης εμφανίζεται. Εισάγετε οποιοδήποτε όνομα χρήστη και κωδικό πρόσβασης και στη συνέχεια υποβάλετε τη φόρμα. Το API επιστρέφει ένα token και η εφαρμογή ανακατευθύνει στον πίνακα ελέγχου.
Η διαδικασία σύνδεσης λειτουργεί: η υποβολή της φόρμας καλεί το API και αποθηκεύει το επιστρεφόμενο token. Ωστόσο, το token αποθηκεύεται στην κατάσταση του component (μνήμη JavaScript), πράγμα που σημαίνει ότι χάνεται κατά την ανανέωση της σελίδας ή όταν ανοίγετε μια νέα καρτέλα. Οι χρήστες θα χρειαστεί να συνδεθούν ξανά μετά από κάθε ανανέωση.
Το πρόβλημα: Η κατάσταση του component (useState) δεν διατηρείται κατά την επαναφόρτωση της σελίδας. Όταν το πρόγραμμα περιήγησης ανανεώνεται, το React επανεκκινεί τα components και οποιαδήποτε κατάσταση αποθηκεύεται στη μνήμη χάνονται.
Στο επόμενο βήμα, θα υλοποιήσετε μόνιμη αποθήκευση διακριτικών χρησιμοποιώντας τα API αποθήκευσης του προγράμματος περιήγησης (localStorage ή sessionStorage) ώστε η αυθεντικοποίηση να διατηρείται κατά την ανανέωση της σελίδας και τις περιόδους σύνδεσης του προγράμματος περιήγησης.
Βήμα 3 — Αποθήκευση ενός Token Χρήστη με sessionStorage και localStorage
Η τοποθεσία αποθήκευσης των διακριτικών καθορίζει τη διάρκεια της συνεδρίας και τις ανταλλαγές ασφαλείας. Το localStorage κρατά τους χρήστες συνδεδεμένους σε διαφορετικές συνεδρίες και καρτέλες του προγράμματος περιήγησης, ενώ το sessionStorage διαγράφεται όταν κλείνει η καρτέλα. Και τα δύο είναι προσβάσιμα μέσω JavaScript, καθιστώντας τα ευάλωτα σε επιθέσεις XSS, αλλά προσφέρουν καλύτερη εμπειρία χρήστη σε σύγκριση με την αποθήκευση διακριτικών μόνο στην κατάσταση ενός component.
Σύγκριση επιλογών αποθήκευσης:
- Κατάσταση στοιχείου (
useState): Χάνεται κατά την ανανέωση της σελίδας. Δεν διατηρείται. sessionStorage: Διατηρείται για τη συνεδρία της καρτέλας του προγράμματος περιήγησης. Διαγράφεται όταν κλείσει η καρτέλα. Δεν κοινοποιείται μεταξύ καρτελών.localStorage: Διατηρείται μεταξύ των συνεδριών και των καρτελών του προγράμματος περιήγησης. Επιβιώνει επανεκκινήσεων του προγράμματος περιήγησης. Κοινόχρηστο σε όλες τις καρτέλες από την ίδια προέλευση.- Cookies: Αποστέλλονται αυτόματα με τα αιτήματα. Απαιτούν προστασία CSRF. Μπορούν να είναι
httpOnly(μη προσβάσιμα από JavaScript, πιο ασφαλή).
Θέμα ασφάλειας: Οποιαδήποτε δεδομένα αποθηκεύονται εκτός της μνήμης του στοιχείου (συμπεριλαμβανομένων των localStorage, sessionStorage και των cookies που είναι προσβάσιμα από JavaScript) είναι ευάλωτα σε επιθέσεις Cross-Site Scripting (XSS). Αν εκτελεστεί κακόβουλος κώδικας στο domain σας, μπορεί να έχει πρόσβαση σε αυτούς τους μηχανισμούς αποθήκευσης. Πάντα να καθαρίζετε τα δεδομένα που εισάγουν οι χρήστες και να χρησιμοποιείτε headers Content Security Policy (CSP) για να μετριάσετε τους κινδύνους XSS.
Αυτό το σεμινάριο καλύπτει το sessionStorage και το localStorage ως σύγχρονες εναλλακτικές των cookies για την αποθήκευση token. Τα cookies καλύπτονται στο Βήμα 8 όταν συζητείται η προστασία CSRF.
Αποθήκευση Συνεδρίας
Για να δοκιμάσετε τα οφέλη της αποθήκευσης εκτός της μνήμης, μετατρέψτε την αποθήκευση στη μνήμη σε sessionStorage. Ανοίξτε το App.js:
nano src/components/App/App.jsΑφαιρέστε την κλήση στο useState και δημιουργήστε δύο νέες συναρτήσεις που ονομάζονται setToken και getToken. Στη συνέχεια, καλέστε τη getToken και αναθέστε τα αποτελέσματα σε μια μεταβλητή που ονομάζεται 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;Η χρήση των ίδιων ονομάτων συναρτήσεων (setToken, getToken) και ονομάτων μεταβλητών (token) σημαίνει ότι το component Login και το υπόλοιπο App.js δεν χρειάζονται αλλαγές. Η διεπαφή παραμένει η ίδια, αλλά η υλοποίηση τώρα χρησιμοποιεί μόνιμη αποθήκευση.
Υλοποιήστε το setToken για να αποθηκεύσετε το token στο sessionStorage. Η μέθοδος setItem απαιτεί ένα κλειδί (string) και μια τιμή (string), οπότε μετατρέψτε το αντικείμενο userToken σε συμβολοσειρά JSON χρησιμοποιώντας το 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;Αποθήκευσε το αρχείο. Ο περιηγητής φορτώνει ξανά αυτόματα. Μετά την υποβολή της φόρμας σύνδεσης, το token αποθηκεύεται στο sessionStorage, αλλά η σελίδα σύνδεσης εξακολουθεί να εμφανίζεται επειδή το React δεν έχει επανασχεδιάσει τη σελίδα. Άνοιξε τα εργαλεία ανάπτυξης του περιηγητή σου (F12) και έλεγξε την καρτέλα Αποθήκευση για να επιβεβαιώσεις ότι το token έχει αποθηκευτεί. Η παρακάτω εικόνα δείχνει τα εργαλεία ανάπτυξης του Firefox· ο Chrome και άλλοι σύγχρονοι περιηγητές έχουν παρόμοια εργαλεία επιθεώρησης αποθήκευσης.
Υλοποιήστε το getToken για να ανακτήσετε το token από το sessionStorage. Η μέθοδος getItem επιστρέφει την αποθηκευμένη συμβολοσειρά (ή null αν δεν υπάρχει τιμή). Αναλύστε τη συμβολοσειρά JSON πίσω σε αντικείμενο και έπειτα επιστρέψτε την ιδιότητα 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;Γιατί να χρησιμοποιήσετε προαιρετική αλυσίδωση (?.): Στην πρώτη επίσκεψη, sessionStorage.getItem('token') επιστρέφει null, και JSON.parse(null) επιστρέφει null. Χωρίς προαιρετική αλυσίδωση, η πρόσβαση στο userToken.token όταν το userToken είναι null προκαλεί TypeError. Ο τελεστής ?. επιστρέφει με ασφάλεια undefined αντί να προκαλέσει σφάλμα.
Αποθηκεύστε και κλείστε το αρχείο. Εάν υπάρχει ένα διακριτικό στο sessionStorage, ο φυλλομετρητής ανανεώνεται αυτόματα και μεταβαίνει στις προστατευμένες σελίδες:
Για να δοκιμάσετε ξανά τη διαδικασία σύνδεσης, διαγράψτε το αποθηκευμένο token. Στα εργαλεία προγραμματιστή του προγράμματος περιήγησής σας, είτε διαγράψτε το token από την καρτέλα Storage είτε εκτελέστε sessionStorage.clear() στην κονσόλα.
Το πρόβλημα: Μετά την είσοδο, το token αποθηκεύεται στο sessionStorage, αλλά η σελίδα σύνδεσης εξακολουθεί να εμφανίζεται. Αυτό συμβαίνει επειδή το React δεν γνωρίζει ότι το token αποθηκεύτηκε και το component χρειάζεται να ξανασχεδιαστεί όταν αλλάξει το token.
Γιατί το React δεν κάνει επανασχεδίαση: Η αποθήκευση δεδομένων στο sessionStorage δεν προκαλεί επανασχεδιάσεις στο React. Το React επανασχεδιάζεται μόνο όταν αλλάζει η κατάσταση (state). Για να το διορθώσετε, χρησιμοποιήστε την κατάσταση (state) του React για να παρακολουθείτε το token και ενημερώστε αυτή την κατάσταση κατά την αποθήκευση ή την ανάκτηση των tokens. Αυτό θα προκαλέσει επανασχεδιασμό και θα ενημερώσει το περιβάλλον χρήστη.
Λύση: Δημιουργήστε ένα προσαρμοσμένο Hook που συνδυάζει λειτουργίες αποθήκευσης με την κατάσταση του React. Αυτό κεντράρει τη λογική διαχείρισης token και την καθιστά επαναχρησιμοποιήσιμη σε διάφορα components.
Δημιουργία Προσαρμοσμένου Token Hook
Τι είναι ένα προσαρμοσμένο Hook: Ένα προσαρμοσμένο Hook είναι μια συνάρτηση JavaScript που χρησιμοποιεί React Hooks (όπως useState, useEffect) και ενσωματώνει επαναχρησιμοποιήσιμη λογική. Σύμφωνα με τη σύμβαση, τα προσαρμοσμένα Hooks ξεκινούν με use* (π.χ., useToken, useAuth). Τα προσαρμοσμένα Hooks σας επιτρέπουν να εξάγετε τη λογική του component σε επαναχρησιμοποιήσιμες συναρτήσεις, καθιστώντας τον κώδικα πιο συντηρήσιμο και δοκιμαστέο.
Άνοιξε ένα νέο αρχείο στον φάκελο App με όνομα useToken.js:
nano src/components/App/useToken.jsΟργάνωση αρχείων: Αυτό το Hook θα μπορούσε να οριστεί απευθείας στο App.js, αλλά η εξαγωγή του σε ξεχωριστό αρχείο δείχνει πώς λειτουργούν τα προσαρμοσμένα Hooks ανεξάρτητα από τα components. Καθώς η εφαρμογή σας μεγαλώνει και επαναχρησιμοποιείτε αυτό το Hook σε πολλά components, η διατήρησή του σε ξεχωριστό αρχείο καθιστά τη συντήρηση και τη δοκιμή πιο εύκολη.
Στο useToken.js, εισάγετε το useState από το react. Δεδομένου ότι αυτό το αρχείο δεν περιέχει JSX, δεν χρειάζεται να εισάγετε το React. Δημιουργήστε και εξάγετε μια συνάρτηση με το όνομα useToken που χρησιμοποιεί το useState για να διαχειρίζεται την κατάσταση του token:
import { useState } from 'react';
export default function useToken() {
const [token, setToken] = useState();
}Πρόσθεσε τη συνάρτηση getToken ως συνάρτηση βέλους μέσα στο useToken. Τοποθέτησέ την πριν από τη δήλωση της κατάστασης και στη συνέχεια αρχικοποίησε το useState με getToken() για να φορτωθεί οποιοδήποτε υπάρχον token κατά την εκκίνηση του component:
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());
}Προσθέστε μια συνάρτηση saveToken που αποθηκεύει το token τόσο στο sessionStorage όσο και στην κατάσταση του React. Η αποθήκευση στην κατάσταση προκαλεί επαναφορά της απόδοσης, ενημερώνοντας το 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);
};
}Επιστρέψτε ένα αντικείμενο με token και setToken (αντιστοιχισμένο σε saveToken). Η χρήση ενός αντικειμένου αντί για πίνακα επιτρέπει στα components να αποσυνθέτουν μόνο τις τιμές που χρειάζονται. Αυτό διατηρεί την ίδια διεπαφή με πριν, οπότε το App.js δεν χρειάζεται αλλαγές:
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
}
}Αποθήκευσε και κλείσε το αρχείο.
Στη συνέχεια, ανοίξτε το App.js:
nano src/components/App/App.jsΑφαιρέστε τις συναρτήσεις getToken και setToken από το App.js και αφαιρέστε την εισαγωγή του useState. Αντ' αυτού, εισάγετε και χρησιμοποιήστε το Hook useToken:
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;Αποθηκεύστε και κλείστε το αρχείο. Ο περιηγητής ανανεώνει αυτόματα. Αφού συνδεθείτε, η εφαρμογή μεταβαίνει αμέσως στη προστατευμένη σελίδα επειδή το useState στο προσαρμοσμένο Hook προκαλεί επανασχεδίαση όταν ενημερώνεται η κατάσταση του token:
Το προσαρμοσμένο Hook αποθηκεύει τα tokens στο sessionStorage και διαχειρίζεται την κατάσταση στο React. Η ανανέωση της σελίδας κρατά τον χρήστη συνδεδεμένο επειδή το token παραμένει στο sessionStorage. Ωστόσο, το άνοιγμα της εφαρμογής σε νέα καρτέλα αποσυνδέει τον χρήστη επειδή το sessionStorage είναι συγκεκριμένο για την καρτέλα, τα δεδομένα δεν μοιράζονται μεταξύ καρτελών και διαγράφονται όταν κλείνει η καρτέλα.
Πότε να χρησιμοποιείτε sessionStorage: Χρησιμοποιήστε sessionStorage όταν θέλετε πιστοποίηση μόνο για τη διάρκεια της συνεδρίας, η οποία διαγράφεται όταν κλείνει η καρτέλα. Αυτό είναι πιο ασφαλές για κοινόχρηστους υπολογιστές.
Πότε να χρησιμοποιείτε localStorage: Χρησιμοποιήστε localStorage όταν θέλετε η πιστοποίηση να παραμείνει ενεργή ανάμεσα σε καρτέλες και περιόδους συνεδρίας του προγράμματος περιήγησης. Αυτό προσφέρει καλύτερη εμπειρία χρήστη, αλλά απαιτεί από τους χρήστες να αποσυνδεθούν ρητά σε κοινόχρηστους υπολογιστές.
Χρήση του localStorage για Αποθήκευση Δεδομένων σε Παράθυρα
localStorage διατηρείται ανάμεσα σε συνεδρίες και καρτέλες του προγράμματος περιήγησης, σε αντίθεση με το sessionStorage που είναι συγκεκριμένο για κάθε καρτέλα. Αυτό σημαίνει ότι οι χρήστες παραμένουν συνδεδεμένοι όταν ανοίγουν νέες καρτέλες ή επανεκκινούν το πρόγραμμα περιήγησης.
Θέματα ασφάλειας: Σε κοινόχρηστους υπολογιστές, τα tokens του localStorage παραμένουν ακόμη και μετά το κλείσιμο του περιηγητή. Ο επόμενος χρήστης θα μπορούσε να έχει πρόσβαση στην εφαρμογή χωρίς να συνδεθεί, αν ο προηγούμενος χρήστης δεν έκανε αποσύνδεση. Για κοινόχρηστους υπολογιστές, εξετάστε:
- Προσθήκη προειδοποιήσεων λήξης συνεδρίας
- Υλοποίηση της λειτουργίας «Αποθήκευση Σύνδεσης» ως επιλογή συμμετοχής
- Χρησιμοποιώντας το
sessionStorageαπό προεπιλογή και τοlocalStorageμόνο όταν ζητείται ρητά - Παροχή ξεκάθαρης λειτουργίας αποσύνδεσης
Για προσωπικές συσκευές, το localStorage προσφέρει καλύτερη εμπειρία χρήστη με ελάχιστο κίνδυνο ασφάλειας όταν συνδυάζεται με κατάλληλη προστασία από XSS.
Για να μετατρέψετε σε localStorage, ανοίξτε το useToken.js:
nano src/components/App/useToken.jsΑντικαταστήστε όλες τις αναφορές του sessionStorage με localStorage. Οι μέθοδοι του API (getItem, setItem) είναι ίδιες:
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
}
}Αποθήκευσε το αρχείο. Ο περιηγητής ανανεώνεται αυτόματα. Σύνδεσου ξανά (το προηγούμενο διακριτικό ήταν στο sessionStorage, όχι στο localStorage). Μετά τη σύνδεση, το διακριτικό διατηρείται στο localStorage, έτσι παραμένεις συνδεδεμένος όταν ανοίγεις νέες καρτέλες ή επανεκκινείς τον περιηγητή.
Έχετε υλοποιήσει μόνιμη αποθήκευση token χρησιμοποιώντας sessionStorage και localStorage, και έχετε δημιουργήσει ένα προσαρμοσμένο Hook useToken που συνδυάζει τις λειτουργίες αποθήκευσης με τη διαχείριση κατάστασης στο React. Το Hook προκαλεί επανασχεδιασμούς όταν αλλάζουν τα tokens, εξασφαλίζοντας ότι το UI ενημερώνεται σωστά. Έχετε επίσης μάθει τα πλεονεκτήματα και τα μειονεκτήματα μεταξύ sessionStorage (ειδικό για την καρτέλα, πιο ασφαλές) και localStorage (παραμένει διαρκώς, καλύτερη εμπειρία χρήστη).
Βήμα 4 — Υλοποίηση Προστατευμένων Διαδρομών με το React Router v6
Οι προστατευμένες διαδρομές απαιτούν αυθεντικοποίηση πριν από την απόδοση. Στο React Router v6, δημιουργείτε ένα περιτυλιγμένο συστατικό που ελέγχει για ένα token και ανακατευθύνει τους μη αυθεντικοποιημένους χρήστες στη σελίδα σύνδεσης, διατηρώντας τον προορισμό που είχαν σκοπό να επισκεφτούν.
Γιατί να χρησιμοποιείτε ένα συστατικό ProtectedRoute: Αντί να ελέγχετε την αυθεντικοποίηση σε κάθε συστατικό ή να χρησιμοποιείτε συνθήκη απόδοσης στο App.js, ένα αφιερωμένο συστατικό ProtectedRoute συγκεντρώνει τη λογική προστασίας διαδρομών. Αυτό καθιστά ευκολότερη την προσθήκη προστατευμένων διαδρομών, τη διαχείριση ανακατευθύνσεων και τη διατήρηση συνεπούς συμπεριφοράς αυθεντικοποίησης.
Δημιουργήστε ένα νέο αρχείο για το στοιχείο ProtectedRoute:
nano src/components/ProtectedRoute/ProtectedRoute.jsΜέσα στο ProtectedRoute.js, εισάγετε το Navigate από το react-router-dom και δημιουργήστε ένα component που ελέγχει για ένα token:
import { Navigate } from 'react-router-dom';
export default function ProtectedRoute({ children, token }) {
if (!token) {
return <Navigate to="/login" replace />;
}
return children;
}Το στοιχείο ProtectedRoute λαμβάνει children (το στοιχείο διαδρομής που θέλουμε να προστατεύσουμε) και token ως ιδιότητες. Αν δεν υπάρχει token, γίνεται ανακατεύθυνση στο /login χρησιμοποιώντας το Navigate με την ιδιότητα replace για να αποφευχθεί η προσθήκη μιας εγγραφής στο ιστορικό του προγράμματος περιήγησης.
Τώρα ενημέρωσε το App.js για να χρησιμοποιεί το 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;Με αυτή την προσέγγιση, οι μη αυθεντικοποιημένοι χρήστες που επισκέπτονται το /dashboard ή το /preferences ανακατευθύνονται στο /login.
Σημείωση: Στο React Router v6, μπορείτε επίσης να χρησιμοποιήσετε το Outlet component για nested routes και προστασία διαδρομών. Για απλούστερες εφαρμογές, το wrapper component ProtectedRoute που εμφανίζεται εδώ είναι αρκετό και πιο εύκολο στην κατανόηση.
Βήμα 5 — Αποκωδικοποίηση JWT Tokens και Έλεγχος Λήξης
Τα JWT tokens περιέχουν πληροφορίες λήξης στο payload τους. Μπορείτε να αποκωδικοποιήσετε το token στον πελάτη για να ελέγξετε αν έχει λήξει χωρίς να κάνετε αίτημα στον διακομιστή, βελτιώνοντας την εμπειρία του χρήστη με άμεση ανατροφοδότηση.
Γιατί να αποκωδικοποιείτε τα tokens στην πλευρά του πελάτη: Παρόλο που θα πρέπει πάντα να επαληθεύετε τα tokens στον διακομιστή, η αποκωδικοποίηση στην πλευρά του πελάτη σας επιτρέπει να ελέγχετε τη λήξη πριν κάνετε κλήσεις API, να εμφανίζετε κατάλληλα μηνύματα σφάλματος και να αποσυνδέετε αυτόματα χρήστες με ληγμένα tokens.
Πρώτα, εγκαταστήστε μια βιβλιοθήκη για την αποκωδικοποίηση JWT. Αν και μπορείτε να αποκωδικοποιήσετε τα JWT χειροκίνητα (είναι κωδικοποιημένα σε base64), η χρήση μιας βιβλιοθήκης διαχειρίζεται τις ακραίες περιπτώσεις και παρέχει καλύτερο χειρισμό σφαλμάτων:
npm install jwt-decodeΕνημερώστε το hook σας useToken.js για να αποκωδικοποιείτε και να επαληθεύετε τα tokens:
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
};
}Η συνάρτηση getToken τώρα αποκωδικοποιεί το JWT και ελέγχει το claim exp (χρονική σήμανση λήξης σε δευτερόλεπτα). Εάν το token έχει λήξει ή είναι άκυρο, το αφαιρεί από την αποθήκευση και επιστρέφει null, αποσυνδέοντας ουσιαστικά τον χρήστη.
Προειδοποίηση: Η αποκωδικοποίηση των tokens από την πλευρά του πελάτη αφορά μόνο την εμπειρία χρήστη. Πάντα να επαληθεύετε τα tokens στον server σας. Κακόβουλοι χρήστες μπορούν να τροποποιήσουν τον κώδικα από την πλευρά του πελάτη ή να στείλουν ψεύτικα tokens. Μην εμπιστεύεστε ποτέ την επικύρωση από την πλευρά του πελάτη για αποφάσεις ασφαλείας.
Βήμα 6 — Υλοποίηση Αποσύνδεσης και Διαχείριση Σφαλμάτων
Η αποσύνδεση αφαιρεί το διακριτικό από την αποθήκευση και κατευθύνει στη σελίδα σύνδεσης. Η διαχείριση σφαλμάτων αποτρέπει την κατάρρευση της εφαρμογής όταν αποτυγχάνουν οι κλήσεις API και παρέχει ανατροφοδότηση στον χρήστη.
Γιατί να υλοποιηθεί η αποσύνδεση: Χωρίς μια λειτουργία αποσύνδεσης, οι χρήστες πρέπει να καθαρίζουν χειροκίνητα την αποθήκευση του προγράμματος περιήγησης ή να περιμένουν να λήξουν τα token. Ένα κουμπί αποσύνδεσης παρέχει έναν σαφή τρόπο για να τερματιστούν οι συνεδρίες και είναι απαραίτητο για κοινόχρηστους υπολογιστές.
Ενημερώστε το συστατικό Dashboard σας ώστε να περιλαμβάνει ένα κουμπί αποσύνδεσης:
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>
);
}Ενημερώστε το App.js ώστε να μεταβιβάζει το removeToken στις προστατευμένες διαδρομές:
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;Τώρα πρόσθεσε διαχείριση σφαλμάτων στο στοιχείο σύνδεσης:
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
};Το στοιχείο σύνδεσης τώρα διαχειρίζεται τα σφάλματα, εμφανίζει καταστάσεις φόρτωσης και παρέχει ανατροφοδότηση στον χρήστη. Η διαχείριση σφαλμάτων αποτρέπει την εφαρμογή από το να καταρρεύσει όταν αποτυγχάνουν οι κλήσεις API και παρέχει στον χρήστη πληροφορίες για το τι πήγε στραβά.
Βήμα 7 — Χρήση του React Context για Αυθεντικοποίηση
Το React Context παρέχει έναν τρόπο να μοιράζεστε την κατάσταση αυθεντικοποίησης μεταξύ των component χωρίς την ανάγκη για prop drilling. Για μεγαλύτερες εφαρμογές, το Context είναι πιο επεκτάσιμο από το να περνάτε props μέσω πολλών επιπέδων component.
Γιατί να χρησιμοποιήσετε το Context για την αυθεντικοποίηση: Όταν η κατάσταση αυθεντικοποίησης απαιτείται σε πολλά στοιχεία (γραμμές πλοήγησης, μενού χρήστη, προστατευμένες διαδρομές), η μεταφορά props γίνεται κουραστική. Το Context παρέχει έναν καθαρό τρόπο πρόσβασης στην κατάσταση αυθεντικοποίησης οπουδήποτε στο δέντρο των στοιχείων.
Δημιουργήστε ένα πλαίσιο ταυτοποίησης:
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;
}Τυλίξτε την εφαρμογή σας με τον 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;Ενημερώστε το ProtectedRoute για να χρησιμοποιεί το context:
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;
}Τώρα κάθε συστατικό μπορεί να έχει πρόσβαση στην κατάσταση αυθεντικοποίησης χρησιμοποιώντας το useAuth(), εξαφανίζοντας την ανάγκη να περνούν οι ιδιότητες μέσα από πολλά επίπεδα.
Βήμα 8 — Κατανόηση της Προστασίας CSRF για Αυθεντικοποίηση με Βάση τα Cookies
Οι επιθέσεις CSRF (Cross-Site Request Forgery) εκμεταλλεύονται το γεγονός ότι οι περιηγητές στέλνουν αυτόματα cookies με τα αιτήματα. Όταν χρησιμοποιείτε πιστοποίηση με βάση τα cookies, πρέπει να εφαρμόσετε προστασία CSRF για να εμποδίσετε κακόβουλους ιστότοπους να κάνουν αυθεντικοποιημένα αιτήματα εκ μέρους των χρηστών.
Γιατί είναι σημαντική η προστασία CSRF: Εάν η αυθεντικοποίησή σας βασίζεται σε cookies (αντί για tokens στο localStorage), μια κακόβουλη ιστοσελίδα μπορεί να εξαπατήσει τους χρήστες να κάνουν αιτήματα με αυθεντικοποίηση προς το API σας. Για παράδειγμα, ένας επιτιθέμενος θα μπορούσε να ενσωματώσει μια φόρμα που υποβάλλει δεδομένα στο API σας και, αν ο χρήστης είναι συνδεδεμένος, ο περιηγητής θα συμπεριλάβει αυτόματα το cookie αυθεντικοποίησης.
Στρατηγικές προστασίας CSRF:
Διακριτικά CSRF: Δημιουργήστε ένα μοναδικό διακριτικό για κάθε συνεδρία και απαιτήστε το στις αιτήσεις. Το διακριτικό πρέπει να ταιριάζει με αυτό που έχει αποθηκευτεί στον διακομιστή.
Χαρακτηριστικό Cookie SameSite: Ορίστε cookies με
SameSite=StrictήSameSite=Laxγια να αποτρέψετε την αποστολή τους σε αιτήματα από άλλους ιστότοπους.Επαλήθευση Επικεφαλίδας Προέλευσης: Ελέγξτε ότι η
OriginήRefererεπικεφαλίδα ταιριάζει με τον τομέα σας.Διπλή Υποβολή Cookie: Αποθηκεύστε το διακριτικό CSRF τόσο σε ένα cookie όσο και σε ένα πεδίο φόρμας, απαιτώντας να ταιριάζουν και τα δύο.
Ακολουθεί ένα παράδειγμα υλοποίησης προστασίας CSRF στον server σας Express:
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'));Στην εφαρμογή σας React, πάρτε το CSRF token πριν πραγματοποιήσετε αιτήσεις με έλεγχο ταυτότητας:
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());
}Σημείωση: Η προστασία CSRF δεν είναι απαραίτητη για την ταυτοποίηση με βάση τα tokens που αποθηκεύονται στο localStorage, επειδή τα tokens πρέπει να περιλαμβάνονται ρητά στις κεφαλίδες των αιτημάτων και δεν αποστέλλονται αυτόματα με τα αιτήματα. Ωστόσο, αν χρησιμοποιείτε cookies για την ταυτοποίηση, η προστασία CSRF είναι απαραίτητη, καθώς τα cookies αποστέλλονται αυτόματα. Ανεξάρτητα από τη μέθοδο ταυτοποίησης που χρησιμοποιείτε, υλοποιήστε πάντα κατάλληλα μέτρα ασφαλείας, όπως HTTPS, επαλήθευση εισόδου και περιορισμό αιτημάτων.
Αντιμετώπιση προβλημάτων
Το διακριτικό δεν διατηρείται μετά την ανανέωση της σελίδας
Πρόβλημα: Μετά την είσοδο, η ανανέωση της σελίδας σας αποσυνδέει.
Λύσεις:
- Επαληθεύστε ότι τα tokens αποθηκεύονται στο
localStorageή στοsessionStorage(ελέγξτε τα DevTools του προγράμματος περιήγησης > Εφαρμογή > Αποθήκευση) - Βεβαιωθείτε ότι η
getToken()εκτελείται κατά την τοποθέτηση του component και αρχικοποιεί σωστά την κατάσταση - Έλεγξε για σφάλματα στην κονσόλα του προγράμματος περιήγησης που μπορεί να εμποδίζουν την ανάκτηση του token
- Ελέγξτε αν η μορφή του token ταιριάζει με ό,τι αναμένει ο κώδικάς σας (αντικείμενο με ιδιότητα
token)
Σφάλμα «Δεν μπορεί να διαβαστεί η ιδιότητα ‘token’ του null»
Πρόβλημα: Παρουσιάζεται σφάλμα όταν γίνεται πρόσβαση στο userToken.token μετά το JSON.parse().
Λύση: Προσθέστε έναν έλεγχο null πριν από την ανάλυση:
const tokenString = localStorage.getItem('token');
if (!tokenString) return null;
const userToken = JSON.parse(tokenString);
return userToken?.token;Η φόρμα σύνδεσης δεν υποβάλλεται
Πρόβλημα: Το πάτημα του κουμπιού υποβολής δεν ενεργοποιεί το αίτημα σύνδεσης.
Λύσεις:
- Βεβαιωθείτε ότι η φόρμα έχει
onSubmit={handleSubmit}και το κουμπί έχειtype="submit" - Ελέγξτε ότι ο διακομιστής API εκτελείται στην σωστή θύρα (προεπιλογή: 8080)
- Ελέγξτε αν το CORS είναι ενεργοποιημένο στον διακομιστή εάν κάνετε αιτήματα διασταυρούμενης προέλευσης
- Ελέγξτε την κονσόλα του προγράμματος περιήγησης για σφάλματα δικτύου ή προβλήματα CORS
Οι Προστατευμένες Διαδρομές Δεν Λειτουργούν
Πρόβλημα: Οι χρήστες μπορούν να έχουν πρόσβαση σε προστατευμένες διαδρομές χωρίς έλεγχο ταυτότητας.
Λύσεις:
- Επιβεβαιώστε ότι το στοιχείο
ProtectedRouteελέγχει το token πριν από την απόδοση - Βεβαιωθείτε ότι το token έχει περαστεί στο
ProtectedRouteή προσπελαύνεται μέσω του Context - Ελέγξτε ότι οι διαδρομές είναι σωστά τυλιγμένες στο
App.js - Επαληθεύστε τη σύνταξη του React Router v6 (ιδιότητα
element, όχι children)
Το διακριτικό λήγει ενώ ο χρήστης είναι ενεργός
Πρόβλημα: Ο χρήστης αποσυνδέεται απρόβλεπτα κατά τη διάρκεια ενεργής χρήσης.
Λύσεις:
- Υλοποιήστε λογική ανανέωσης διακριτικών χρησιμοποιώντας διακριτικά ανανέωσης
- Ελέγξτε τη λήξη του token πριν κάνετε κλήσεις API
- Προσθήκη αυτόματης ανανέωσης διακριτικού όταν πλησιάζει η λήξη
- Σκεφτείτε να επεκτείνετε την ημερομηνία λήξης των διακριτικών για ενεργούς χρήστες
Το localStorage είναι απενεργοποιημένο ή υπερβλήθηκε το όριο
Πρόβλημα: Τα tokens δεν αποθηκεύονται σε ιδιωτική/ανώνυμη λειτουργία ή η χωρητικότητα αποθήκευσης είναι πλήρης.
Λύσεις:
- Ελέγξτε αν το
localStorageείναι διαθέσιμο:if (typeof(Storage) !== "undefined") - Διαχείριση
QuotaExceededErrorκατά την ορισμό αντικειμένων - Επιστροφή στο
sessionStorageή στην αποθήκευση στη μνήμη - Καθαρίστε παλιά tokens πριν αποθηκεύσετε νέα
- Ενημερώστε τους χρήστες εάν η αποθήκευση δεν είναι διαθέσιμη
Πολλαπλές καρτέλες δεν συγχρονίζουν την κατάσταση σύνδεσης
Πρόβλημα: Η αποσύνδεση σε μια καρτέλα δεν αποσυνδέει σε άλλες καρτέλες.
Λύση: Ακούστε για γεγονότα storage για να συγχρονίσετε την κατάσταση μεταξύ των καρτελών:
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 Επιστρέφει Διαφορετική Μορφή Token
Πρόβλημα: Το API σας επιστρέφει { accessToken: '...'' } αντί για { token: '...' }.
Λύση: Ενημερώστε τον κώδικα ώστε να ταιριάζει με τη μορφή απάντησης του API σας:
// If API returns { accessToken: '...' }
const token = userToken?.accessToken;
// Or if API returns { data: { token: '...' } }
const token = userToken?.data?.token;Σφάλματα 401 Μη Εξουσιοδοτημένη Πρόσβαση
Πρόβλημα: Το API επιστρέφει 401 ακόμη και με έγκυρο token.
Λύσεις:
- Ελέγξτε ότι το token αποστέλλεται στις κεφαλίδες του αιτήματος:
Authorization: Bearer $ {token} - Έλεγχος λήξης του token (αποκωδικοποίηση JWT για να δείτε το πεδίο
exp) - Βεβαιωθείτε ότι η μορφή του token ταιριάζει με αυτή που αναμένει ο διακομιστής
- Ελέγξτε αν ο διακομιστής επικυρώνει σωστά τα διακριτικά
- Ελέγξτε αν το διακριτικό χρειάζεται ανανέωση
Συχνές Ερωτήσεις
1. Πώς μπορώ να προστατεύσω διαδρομές στο React;
Χρησιμοποιήστε ένα συστατικό περιτύλιξης ProtectedRoute που ελέγχει την πιστοποίηση πριν από την απόδοση. Στο React Router v6, τυλίξτε τις προστατευμένες διαδρομές με ένα συστατικό που ελέγχει αν υπάρχει το token. Αν δεν βρεθεί token, ανακατευθύνετε στη σελίδα σύνδεσης χρησιμοποιώντας το Navigate:
function ProtectedRoute({ children, token }) {
if (!token) {
return <Navigate to="/login" replace />;
}
return children;
}Στη συνέχεια, χρησιμοποιήστε το στις διαδρομές σας:
<Route
path="/dashboard"
element={
<ProtectedRoute token={token}>
<Dashboard />
</ProtectedRoute>
}
/>2. Πού θα πρέπει να αποθηκεύω τα διαπιστευτήρια ελέγχου ταυτότητας;
Αποθηκεύστε τα tokens στο localStorage για διατήρηση μεταξύ των συνεδριών ή στο sessionStorage για συνεδρίες συγκεκριμένων καρτελών. Και τα δύο είναι προσβάσιμα από JavaScript, καθιστώντας τα ευάλωτα σε επιθέσεις XSS, οπότε πάντα καθαρίζετε την είσοδο χρήστη και χρησιμοποιείτε HTTPS.
localStorage: Διατηρείται ανάμεσα σε συνεδρίες και καρτέλες του προγράμματος περιήγησης. Χρησιμοποιείται για τη λειτουργία «θυμήσου με».sessionStorage: Διαγράφεται όταν κλείνει η καρτέλα. Χρησιμοποιείται για πιο ασφαλή, μόνο για τη διάρκεια της συνεδρίας, αυθεντικοποίηση.- Cookies: Αποστέλλονται αυτόματα με τα αιτήματα και απαιτούν προστασία CSRF. Χρησιμοποιήστε με τη σημαία
httpOnlyγια πρόσβαση μόνο από την πλευρά του διακομιστή.
Για εφαρμογές παραγωγής, σκεφτείτε να χρησιμοποιήσετε ασφαλή cookies τύπου httpOnly που διαχειρίζεται το backend σας, τα οποία είναι λιγότερο ευάλωτα σε επιθέσεις XSS.
3. Είναι ασφαλές το localStorage για την αυθεντικοποίηση;
localStorage είναι ευάλωτο σε επιθέσεις XSS αλλά είναι αποδεκτό για πολλές εφαρμογές όταν συνδυάζεται με σωστά μέτρα ασφαλείας. Οποιοσδήποτε κώδικας JavaScript που εκτελείται στον τομέα σας μπορεί να έχει πρόσβαση στο localStorage, οπότε αν ένας επιτιθέμενος εισάγει κακόβουλο κώδικα, μπορεί να κλέψει tokens.
Στρατηγικές μετριασμού:
- Αποστείρωση όλων των εισόδων χρήστη για την πρόληψη XSS
- Χρησιμοποιήστε επικεφαλίδες Πολιτικής Ασφάλειας Περιεχομένου (CSP)
- Υλοποιήστε λήξη διακριτικών και ανανέωση διακριτικών
- Χρησιμοποιήστε HTTPS για να αποτρέψετε επιθέσεις τύπου «άνδρας στη μέση»
- Σκεφτείτε τα cookies httpOnly για ευαίσθητες εφαρμογές
Για εφαρμογές υψηλής ασφάλειας (τραπεζικές, υγειονομική περίθαλψη), προτιμήστε τα cookies httpOnly με προστασία CSRF, καθώς δεν είναι προσβάσιμα από JavaScript.
4. Οι εφαρμογές React χρειάζονται OAuth;
Απαιτείται OAuth κατά την αυθεντικοποίηση με παρόχους τρίτων (Google, GitHub, Facebook), αλλά δεν είναι απαραίτητο για προσαρμοσμένη αυθεντικοποίηση. Εάν δημιουργείτε το δικό σας σύστημα αυθεντικοποίησης με όνομα χρήστη/κωδικό πρόσβασης, μπορείτε να χρησιμοποιήσετε αυθεντικοποίηση με βάση τα token όπως φαίνεται σε αυτό το σεμινάριο.
Χρησιμοποιήστε OAuth όταν:
- Οι χρήστες θα πρέπει να συνδεθούν με κοινωνικούς λογαριασμούς
- Θέλετε να αναθέσετε τον έλεγχο ταυτότητας σε έναν αξιόπιστο πάροχο
- Πρέπει να έχετε πρόσβαση σε API τρίτων εκ μέρους των χρηστών
Χρησιμοποιήστε προσαρμοσμένη ταυτοποίηση όταν:
- Ελέγχετε τη βάση δεδομένων χρηστών
- Χρειάζεστε πλήρη έλεγχο της διαδικασίας ελέγχου ταυτότητας
- Θέλεις να αποφύγεις εξαρτήσεις από τρίτους
Πολλές εφαρμογές χρησιμοποιούν μια υβριδική προσέγγιση: το OAuth για κοινωνική σύνδεση και προσαρμοσμένη αυθεντικοποίηση για email/κωδικό πρόσβασης.
5. Πώς διαχειρίζομαι την λήξη του token;
Αποκρυπτογραφήστε το JWT token στον πελάτη για να ελέγξετε τη λήξη, και στη συνέχεια ανανεώστε ή αποσυνδέστε τον χρήστη. Χρησιμοποιήστε μια βιβλιοθήκη όπως η jwt-decode για να διαβάσετε το πεδίο 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');
}Για την παραγωγή, εφαρμόστε tokens ανανέωσης: tokens πρόσβασης βραχείας διάρκειας (15-30 λεπτά) και tokens ανανέωσης μεγαλύτερης διάρκειας (7-30 ημέρες) που μπορούν να χρησιμοποιηθούν για να αποκτήσετε νέα tokens πρόσβασης χωρίς επαναπιστοποίηση.
6. Πώς μπορώ να υλοποιήσω τη λειτουργία «Να με θυμάσαι»;
Αποθηκεύστε τα διακριτικά στο localStorage για μόνιμες συνεδρίες και στο sessionStorage για προσωρινές συνεδρίες. Προσθέστε ένα πλαίσιο επιλογής στη φόρμα σύνδεσής σας:
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));
}
};Όταν επιλεγεί το rememberMe, το διακριτικό παραμένει μεταξύ των συνεδριών του προγράμματος περιήγησης. Διαφορετικά, διαγράφεται όταν κλείνει η καρτέλα.
Συμπέρασμα
Η αυθεντικοποίηση σε εφαρμογές React απαιτεί την εξισορρόπηση της ασφάλειας, της εμπειρίας χρήστη και της διαχειρισιμότητας. Με την κεντροποίηση της λογικής αυθεντικοποίησης σε προσαρμοσμένα hooks ή στο React Context, την υλοποίηση προστατευμένων διαδρομών με το React Router v6 και την επιλογή κατάλληλων στρατηγικών αποθήκευσης token, μπορείτε να δημιουργήσετε ασφαλή και φιλικά προς τον χρήστη ροές αυθεντικοποίησης.
Βασικές αρχές που πρέπει να θυμάστε:
- Αποθηκεύστε τα tokens στην αποθήκευση του προγράμματος περιήγησης (
localStorageήsessionStorage) για να παραμείνουν διαρκώς κατά την ανανέωση της σελίδας - Χρησιμοποιήστε προστατευμένα components διαδρομής για να επιβάλετε τις απαιτήσεις αυθεντικοποίησης
- Αποκωδικοποιήστε τα JWT tokens στον πελάτη για να ελέγξετε την ημερομηνία λήξης, αλλά επαληθεύετε πάντα στον διακομιστή
- Υλοποιήστε διαχείριση σφαλμάτων και καταστάσεις φόρτωσης για καλύτερη εμπειρία χρήστη
- Λάβετε υπόψη την προστασία CSRF κατά τη χρήση αυθεντικοποίησης με βάση τα cookies
- Χρησιμοποιήστε το React Context για την κατάσταση ελέγχου ταυτότητας σε μεγαλύτερες εφαρμογές ώστε να αποφύγετε το prop drilling
Κάθε λύση αποθήκευσης και προσέγγιση πιστοποίησης έχει πλεονεκτήματα και μειονεκτήματα. Το localStorage προσφέρει ευκολία αλλά απαιτεί προστασία από XSS. Το sessionStorage είναι πιο ασφαλές αλλά λιγότερο βολικό. Τα cookies απαιτούν προστασία από CSRF αλλά είναι λιγότερο ευάλωτα σε XSS. Επιλέξτε με βάση τις απαιτήσεις ασφαλείας και τους στόχους εμπειρίας χρήστη σας.
Καθώς η εφαρμογή σας αναπτύσσεται, εξετάστε το ενδεχόμενο υλοποίησης ανανεώσιμων διακριτικών, πολυπαραγοντικής αυθεντικοποίησης και δυνατοτήτων διαχείρισης συνεδριών. Για εφαρμογές παραγωγής, χρησιμοποιείτε πάντα HTTPS, εφαρμόζετε περιορισμό ρυθμού και ακολουθείτε τις καλύτερες πρακτικές ασφαλείας.
Αν θέλετε να διαβάσετε περισσότερα tutorials για το React, επισκεφθείτε τη σελίδα μας με Θέματα React ή εξερευνήστε σχετικά tutorials όπως Πώς να Δημιουργήσετε μια Εφαρμογή To-Do Χρησιμοποιώντας Django και React και Πώς να Αναπτύξετε μια Εφαρμογή React με Nginx σε Ubuntu.
Ευχαριστούμε που μάθατε με την Κοινότητα Linux-Console.net.








