ReactJS: Cách thêm xác thực đăng nhập vào ứng dụng React
Giới thiệu
Nhiều ứng dụng web là sự kết hợp của các trang công khai và riêng tư. Trang công khai có sẵn cho bất kỳ ai, trong khi trang riêng tư yêu cầu người dùng đăng nhập. Bạn có thể sử dụng xác thực để quản lý người dùng nào có quyền truy cập vào các trang nào. Ứng dụng React của bạn sẽ cần xử lý các tình huống trong đó người dùng cố gắng truy cập trang riêng tư trước khi họ đăng nhập và bạn sẽ cần lưu thông tin đăng nhập khi họ đã xác thực thành công.
Trong bài hướng dẫn này, bạn sẽ tạo một ứng dụng React bằng hệ thống xác thực dựa trên mã thông báo. Bạn sẽ tạo một API giả sẽ trả về mã thông báo của người dùng, xây dựng trang đăng nhập sẽ tìm nạp mã thông báo và kiểm tra xác thực mà không cần định tuyến lại người dùng. Nếu người dùng chưa được xác thực, bạn sẽ tạo cơ hội cho họ đăng nhập và sau đó cho phép họ tiếp tục mà không cần điều hướng đến trang đăng nhập chuyên dụng. Khi bạn xây dựng ứng dụng, bạn sẽ khám phá các phương pháp lưu trữ mã thông báo khác nhau và sẽ tìm hiểu tính bảo mật cũng như kinh nghiệm đánh đổi cho mỗi cách tiếp cận. Hướng dẫn này sẽ tập trung vào việc lưu trữ mã thông báo trong localStorage và sessionStorage.
Đến cuối hướng dẫn này, bạn sẽ có thể thêm xác thực vào ứng dụng React và tích hợp các chiến lược đăng nhập và lưu trữ mã thông báo vào một quy trình làm việc hoàn chỉnh của người dùng.
Bước 1 - Tạo một dự án trống
Trong bước này, bạn sẽ tạo một dự án mới bằng Create React App. Sau đó, bạn sẽ xóa dự án mẫu và các tệp liên quan được cài đặt khi bạn khởi động dự án. Cuối cùng, bạn sẽ tạo một cấu trúc tệp đơn giản để tổ chức các component của mình. Điều này sẽ cung cấp cho bạn một cơ sở vững chắc để xây dựng ứng dụng mẫu của hướng dẫn này để tạo style trong bước tiếp theo.
Để bắt đầu, hãy thực hiện một dự án mới. Bạn mở terminal và chạy tập lệnh sau để cài đặt một dự án mới bằng cách sử dụng create-react-app
:
npx create-react-app auth-tutorial
Sau khi dự án kết thúc, hãy thay đổi vào thư mục:
cd auth-tutorial
Khởi động dự án với lệnh:
npm start
http://localhost:3000/
. Nếu bạn đang chạy điều này từ một máy chủ từ xa, địa chỉ sẽ là .http://your_domain:3000
Trình duyệt của bạn sẽ tải với một ứng dụng React đơn giản được bao gồm như một phần của Create React App:
Bạn sẽ xây dựng một tập hợp các component tùy chỉnh hoàn toàn mới, vì vậy bạn sẽ cần bắt đầu bằng cách xóa một số mã soạn sẵn để bạn có thể có một dự án trống.
Để bắt đầu, hãy mở component src/App.js
. Đây là component gốc được đưa vào trang. Tất cả các component sẽ bắt đầu từ đây. Bạn sửa lại file để trong đó chỉ còn chứa như sau:
import './App.css';
function App() {
return <></>;
}
export default App;
Mở một terminal khác và thực hiện thao tác xóa file logo.svg:
rm src/logo.svg
Tạo thư mục components:
mkdir src/components
Tạo thư mục App:
mkdir src/components/App
Di chuyển các file App.* vào thư mục App:
mv src/App.* src/components/App
Mở file index.js và chỉnh sửa:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App/App';
import * as serviceWorker from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
Lưu file lại và quay lại trang web ta được một trang web trống.
Bây giờ bạn đã hoàn thành dự án Tạo ứng dụng React mẫu, hãy tạo một cấu trúc tệp đơn giản. Điều này sẽ giúp bạn giữ cho các component của bạn cô lập và độc lập.
Tạo một thư mục được gọi components
trong thư mục src
, components sẽ là nơi chứa tất cả các component tùy chỉnh của bạn.
Bước 2 - Xây dựng trang đăng nhập
Trong bước này, bạn sẽ tạo một trang đăng nhập cho ứng dụng của mình. Bạn sẽ bắt đầu bằng cách cài đặt Bộ định tuyến React và tạo các component để đại diện cho một ứng dụng đầy đủ. Sau đó, bạn sẽ hiển thị trang đăng nhập trên bất kỳ route nào để người dùng của bạn có thể đăng nhập vào ứng dụng mà không bị chuyển hướng đến trang mới.
Đến cuối bước này, bạn sẽ có một ứng dụng cơ bản sẽ hiển thị trang đăng nhập khi người dùng chưa đăng nhập vào ứng dụng.
Để bắt đầu, hãy cài đặt React Router npm
. Có hai phiên bản khác nhau: phiên bản web và phiên bản gốc để sử dụng với React Native. Cài đặt phiên bản web:
npm install react-router-dom
Gói sẽ được cài đặt và bạn sẽ nhận được thông báo khi quá trình cài đặt hoàn tất. Thông điệp của bạn có thể hơi khác một chút:
added 3 packages, and audited 1414 packages in 6s
Tiếp theo, tạo hai component là Dashboard
và Preferences
hoạt động như các trang riêng tư. Chúng sẽ đại diện cho các component mà người dùng sẽ không thấy cho đến khi họ đăng nhập thành công vào ứng dụng.
Đầu tiên, hãy tạo các thư mục:
mkdir src/components/Dashboard mkdir src/components/Preferences
Sau đó, tạo Dashboard.js
trong thư mục Dashboard.
Bên trong Dashboard.js
, hãy thêm một thẻ <h2>
có nội dung là Dashboard
:
export default function Dashboard() { return( <h2>Dashboard</h2> ); }
Lưu file lại.
Tương tự, ta cũng tạo Preferences.js cho thư mục Preferences
rồi thêm nội dung:
export default function Preferences() { return( <h2>Preferences</h2> ); }
Lưu file lại.
Bây giờ bạn đã có một số component, bạn cần import các component và tạo các route bên trong App.js
. Hãy xem hướng dẫn Cách xử lý định tuyến trong ứng dụng React với React Router để biết giới thiệu đầy đủ về định tuyến trong ứng dụng React.
Để bắt đầu, hãy mở App.js
, sau đó, import Dashboard
và Preferences
được đánh dấu sau:
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';
function App() {
return (
<></>
);
}
export default App;
Tiếp theo, import BrowserRouter
, Switch
và Route
từ react-router-dom
:
import './App.css';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';
function App() {
return (
<></>
);
}
export default App;
Thêm thẻ <div>
với một className
là wrapper
và một thẻ <h1>
để làm mẫu cho ứng dụng. Đảm bảo rằng bạn đang import App.css
để có thể áp dụng các style.
Tiếp theo, tạo các route cho các component Dashboard
và Preferences
. Thêm BrowserRouter
, sau đó thêm một component Switch
. Bên trong Switch
, thêm một Route
với path
cho mỗi component:
import './App.css';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';
function App() {
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Switch>
<Route path="/dashboard">
<Dashboard />
</Route>
<Route path="/preferences">
<Preferences />
</Route>
</Switch>
</BrowserRouter>
</div>
);
}
export default App;
Lưu file lại.
Bước cuối cùng là thêm một số padding vào chính <div>
để component của bạn không nằm trực tiếp ở rìa của trình duyệt. Để làm điều này, bạn sẽ thay đổi CSS.
Mở App.css
:
Thay thế nội dung bằng một class .wrapper
với padding
là 20px
:
Lưu file lại. Khi bạn làm như vậy, trình duyệt sẽ tải lại và bạn sẽ tìm thấy các component cơ bản của mình:
Kiểm tra từng route. Nếu bạn truy cập http://localhost:3000/dashboard
, bạn sẽ tìm thấy trang tổng quan:
Các route của bạn đã đang hoạt động như mong đợi, nhưng có một vấn đề nhỏ. Route /dashboard
phải là một trang được bảo vệ và người dùng chưa được xác thực sẽ không thể xem được route này. Có nhiều cách khác nhau để xử lý một trang riêng tư. Ví dụ: bạn có thể tạo một route mới cho trang đăng nhập và sử dụng Bộ định tuyến React để chuyển hướng nếu người dùng chưa đăng nhập. Đây là một cách tiếp cận tốt, nhưng người dùng sẽ mất route và phải điều hướng trở lại trang mà họ muốn xem ban đầu.
Một tùy chọn ít xâm phạm hơn là tạo trang đăng nhập bất kể route là gì. Với cách tiếp cận này, bạn sẽ hiển thị trang đăng nhập nếu không có mã thông báo người dùng được lưu trữ và khi người dùng đăng nhập, họ sẽ ở trên cùng một route mà họ đã truy cập ban đầu. Điều đó có nghĩa là nếu người dùng truy cập /dashboard
, họ sẽ vẫn ở trên route /dashboard
sau khi đăng nhập.
Để bắt đầu, hãy tạo một thư mục mới cho component Login
. Tiếp theo, mở Login.js
và tạo một biểu mẫu cơ bản với một <button>
submit và hai <input>
cho tên người dùng và mật khẩu. Đảm bảo đặt kiểu nhập cho mật khẩu thành password
:
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> ) }
Để biết thêm về các biểu mẫu trong React, hãy xem hướng dẫn Cách tạo biểu mẫu trong React.
Tiếp theo, thêm một thẻ <h1>
yêu cầu người dùng đăng nhập. Kết hợp các <form>
và <h1>
vào trong một thẻ <div>
với một className là login-wrapper. Cuối cùng, import Login.css:
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> ) }
Lưu file lại.
Bây giờ bạn đã có một component Login
cơ bản, bạn sẽ cần thêm một số style. Mở Login.css
. Căn giữa component trên trang bằng cách thêm một display
là flex
, sau đó đặt flex-direction
thành column
để căn chỉnh các phần tử theo chiều dọc và thêm align-items
là center
vào để làm cho component được căn giữa trong trình duyệt:
.login-wrapper { display: flex; flex-direction: column; align-items: center; }
Để biết thêm thông tin về cách sử dụng Flexbox, hãy xem CSS Flexbox Container của chúng tôi
Lưu file lại.
Cuối cùng, bạn sẽ cần phải hiển thị nó bên trong App.js
nếu không có mã thông báo người dùng.
Mở App.js
, ở Bước 4 bạn sẽ khám phá các tùy chọn để lưu trữ mã thông báo. Hiện tại, bạn có thể lưu trữ mã thông báo trong bộ nhớ bằng Hook useState.
Import useState
từ react
, sau đó gọi useState
và đặt giá trị trả về thành token
và setToken
:
import { useState } from 'react'; import { BrowserRouter, Route, Switch } 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> <Switch> <Route path="/dashboard"> <Dashboard /> </Route> <Route path="/preferences"> <Preferences /> </Route> </Switch> </BrowserRouter> </div> ); } export default App;
Import component Login
. Thêm một câu lệnh điều kiện để hiển thị Login
nếu token
là sai.
Truyền phương thức setToken
cho component Login
:
import { useState } from 'react'; import { BrowserRouter, Route, Switch } 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> <Switch> <Route path="/dashboard"> <Dashboard /> </Route> <Route path="/preferences"> <Preferences /> </Route> </Switch> </BrowserRouter> </div> ); } export default App;
Hiện tại, không có mã thông báo nào; trong bước tiếp theo, bạn sẽ gọi một API và đặt mã thông báo với giá trị trả về.
Lưu file lại. Khi bạn làm như vậy, trình duyệt sẽ tải lại và bạn sẽ thấy trang đăng nhập. Lưu ý rằng nếu bạn truy cập http://localhost:3000/dashboard
, bạn sẽ vẫn tìm thấy trang đăng nhập vì mã thông báo chưa được đặt:
Như vậy trong bước này, bạn đã tạo một ứng dụng có component riêng tư và component đăng nhập sẽ hiển thị cho đến khi bạn đặt mã thông báo. Bạn cũng đã định cấu hình các route để hiển thị các trang và thêm dấu kiểm để hiển thị component Login
trên mọi route nếu người dùng chưa đăng nhập vào ứng dụng.
Trong bước tiếp theo, bạn sẽ tạo một API cục bộ sẽ trả về mã thông báo người dùng. Bạn sẽ gọi API từ component Login
và lưu mã thông báo vào bộ nhớ khi thành công.
Bước 3 - Tạo API mã thông báo
Trong bước này, bạn sẽ tạo một API cục bộ để tìm nạp mã thông báo của người dùng. Bạn sẽ xây dựng một API giả sử dụng Node.js sẽ trả về mã thông báo người dùng. Sau đó, bạn sẽ gọi API đó từ trang đăng nhập của mình và hiển thị component sau khi bạn truy xuất thành công mã thông báo. Đến cuối bước này, bạn sẽ có một ứng dụng có trang đăng nhập đang hoạt động và các trang được bảo vệ sẽ chỉ có thể truy cập được sau khi đăng nhập.
Bạn sẽ cần một máy chủ để hoạt động như một chương trình phụ trợ sẽ trả về mã thông báo. Bạn có thể tạo một máy chủ nhanh chóng bằng cách sử dụng Node.js và framework web Express. Để được giới thiệu chi tiết về cách tạo máy chủ Express, hãy xem hướng dẫn Máy chủ Express cơ bản trong Node.js.
Để bắt đầu, hãy cài đặt express
. Vì máy chủ không phải là yêu cầu của bản dựng cuối cùng, hãy đảm bảo cài đặt dưới dạng devDependency
.
Bạn cũng sẽ cần phải cài đặt cors. Thư viện này sẽ cho phép chia sẻ tài nguyên nguồn gốc chéo cho tất cả các route.
Cảnh báo: Không bật CORS cho tất cả các route trong ứng dụng production. Điều này có thể dẫn đến các lỗ hổng bảo mật.
npm install --save-dev express cors
Khi quá trình cài đặt hoàn tất, bạn sẽ nhận được thông báo thành công:
added 1 package, and audited 1415 packages in 7s
Tiếp theo, mở một tệp mới có tên server.js
trong thư mục gốc của ứng dụng của bạn. Không thêm tệp này vào thư mục /src
vì bạn không muốn nó là một phần của bản dựng cuối cùng.
Import express
, sau đó khởi tạo một ứng dụng mới bằng cách gọi express()
và lưu kết quả vào một biến có tên là app
:
const express = require('express'); const app = express();
Sau khi tạo app
, hãy thêm cors
làm middleware. Đầu tiên, import cors
, sau đó thêm nó vào ứng dụng bằng cách gọi phương thức use
trên app
:
const express = require('express'); const cors = require('cors'); const app = express(); app.use(cors());
Tiếp theo, hãy thiết lập một trình nghe route cụ thể với app.use
. Đối số đầu tiên là đường dẫn mà ứng dụng sẽ lắng nghe và đối số thứ hai là một callback sẽ chạy khi ứng dụng phục vụ đường dẫn. Lệnh gọi lại nhận một đối số req
, chứa dữ liệu yêu cầu và một đối số res
xử lý kết quả.
Thêm một trình xử lý cho đường dẫn /login
. Gọi res.send
bằng một đối tượng JavaScript có chứa mã thông báo:
const express = require('express'); const cors = require('cors') const app = express(); app.use(cors()); app.use('/login', (req, res) => { res.send({ token: 'test123' }); });
Cuối cùng, chạy máy chủ trên cổng 8080
bằng cách sử dụng 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/login'));
Lưu file lại. Trong cửa sổ hoặc tab đầu cuối mới, hãy khởi động máy chủ:
node server.js
Bạn sẽ nhận được phản hồi cho biết máy chủ đang khởi động:
Output
API is running on http://localhost:8080/login
Truy cập http://localhost:8080/login
và bạn sẽ tìm thấy đối tượng JSON của mình .
Khi bạn tìm nạp mã thông báo trong trình duyệt của mình, bạn đang đưa ra yêu cầu GET
, nhưng khi bạn gửi biểu mẫu đăng nhập, bạn sẽ đưa ra yêu cầu POST
. Đó không phải là một vấn đề. Khi bạn thiết lập route của mình với app.use
, Express sẽ xử lý tất cả các yêu cầu như nhau. Trong ứng dụng production, bạn nên cụ thể hơn và chỉ cho phép một số phương thức yêu cầu nhất định cho từng route.
Bây giờ bạn có một máy chủ API đang chạy, bạn cần thực hiện yêu cầu từ trang đăng nhập của mình.
Mở Login.js
ra, trong bước trước, bạn đã truyền một prop mới được gọi là setToken
đến component Login
. Thêm vào PropType
từ prop mới và destructure cấu trúc đối tượng prop để đẩy prop setToken
ra.
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 }
Tiếp theo, tạo một state cục bộ để bắt Username
và Password
. Vì bạn không cần phải thiết lập dữ liệu theo cách thủ công, hãy làm cho các thành phần không cần kiểm soát là <input>
. Bạn có thể tìm thông tin chi tiết về các thành phần không được kiểm soát trong Cách tạo biểu mẫu trong React.
import { 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 };
Tiếp theo, tạo một hàm để thực hiện một yêu cầu POST
đến máy chủ. Trong một ứng dụng lớn, bạn sẽ thêm chúng vào một thư mục riêng. Trong ví dụ này, bạn sẽ thêm dịch vụ trực tiếp vào component. Hãy xem hướng dẫn Cách gọi API Web bằng Hook useEffect trong React để có cái nhìn chi tiết về cách gọi API trong các component React.
Tạo một hàm async
được gọi là loginUser
. Hàm sẽ coi credentials
như một đối số, sau đó nó sẽ gọi phương thức fetch
bằng cách sử dụng tùy chọn POST
:
import { 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 }) {
...
Cuối cùng, tạo một trình xử lý gửi biểu mẫu có tên handleSubmit
sẽ gọi loginUser
với username
và password
. Gọi setToken
với một kết quả thành công. Gọi handleSubmit
bằng trình xử lý sự kiện onSubmit
trên <form>
:
import { 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 };
Lưu ý: Trong một ứng dụng đầy đủ, bạn sẽ cần xử lý các tình huống trong đó thành phần ngắt kết nối trước khi Promise được giải quyết. Hãy xem hướng dẫn Cách gọi API Web bằng Hook useEffect trong React để biết thêm thông tin.
Lưu file lại. Đảm bảo rằng API cục bộ của bạn vẫn đang chạy, sau đó mở trình duyệt http://localhost:3000/dashboard
.
Bạn sẽ thấy trang đăng nhập thay vì trang tổng quan. Điền vào và gửi biểu mẫu và bạn sẽ nhận được mã thông báo web sau đó chuyển hướng đến trang cho bảng điều khiển.
Bây giờ bạn có một API cục bộ đang hoạt động và một ứng dụng yêu cầu mã thông báo bằng tên người dùng và mật khẩu. Nhưng vẫn còn một vấn đề. Mã thông báo hiện được lưu trữ bằng state cục bộ, có nghĩa là nó được lưu trữ trong bộ nhớ JavaScript. Nếu bạn mở một cửa sổ, tab mới hoặc thậm chí chỉ làm mới trang, bạn sẽ mất mã thông báo và người dùng sẽ cần đăng nhập lại. Điều này sẽ được giải quyết trong bước tiếp theo.
Như vậy trong bước này, bạn đã tạo một API cục bộ và một trang đăng nhập cho ứng dụng của mình. Bạn đã học cách tạo máy chủ Node để gửi mã thông báo và cách gọi máy chủ và lưu trữ mã thông báo từ một component đăng nhập. Trong bước tiếp theo, bạn sẽ học cách lưu trữ mã thông báo người dùng để một phiên sẽ tồn tại trên các tab hoặc lần làm mới trang.
Bước 4 - Lưu trữ Mã người dùng với sessionStorage
và localStorage
Trong bước này, bạn sẽ lưu trữ mã thông báo người dùng. Bạn sẽ triển khai các tùy chọn lưu trữ mã thông báo khác nhau và tìm hiểu ý nghĩa bảo mật của từng cách tiếp cận. Cuối cùng, bạn sẽ tìm hiểu các cách tiếp cận khác nhau sẽ thay đổi trải nghiệm người dùng như thế nào khi người dùng mở tab mới hoặc đóng phiên.
Khi kết thúc bước này, bạn sẽ có thể chọn phương pháp lưu trữ dựa trên các mục tiêu cho ứng dụng của mình.
Có một số tùy chọn để lưu trữ mã thông báo. Mọi lựa chọn đều có chi phí và lợi ích. Tóm lại, các tùy chọn là: lưu trữ trong bộ nhớ JavaScript, lưu trữ trong sessionStorage, lưu trữ trong localStorage và lưu trữ trong cookie. Đánh đổi chính là bảo mật. Bất kỳ thông tin nào được lưu trữ bên ngoài bộ nhớ của ứng dụng hiện tại đều dễ bị tấn công Cross-Site Scripting (XSS). Điều nguy hiểm là nếu một người dùng độc hại có thể tải mã vào ứng dụng của bạn, nó có thể truy cập localStorage
và sessionStorage
bất kỳ cookie nào cũng có thể truy cập được vào ứng dụng của bạn. Lợi ích của các phương pháp lưu trữ không dùng bộ nhớ là bạn có thể giảm số lần người dùng cần đăng nhập để tạo trải nghiệm người dùng tốt hơn.
Hướng dẫn này sẽ trình bày sessionStorage
và localStorage
vì chúng hiện đại hơn so với việc sử dụng cookie.
Lưu trữ phiên
Để kiểm tra lợi ích của việc lưu trữ bên ngoài bộ nhớ, hãy chuyển đổi bộ nhớ trong bộ nhớ sang sessionStorage
.
Mở App.js
, xóa lời gọi đến useState
và tạo hai hàm mới có tên setToken
và getToken
. Sau đó, gọi getToken
và gán kết quả cho một biến được gọi là token
:
import { BrowserRouter, Route, Switch } 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"> ... </div> ); } export default App;
Vì bạn đang sử dụng cùng một hàm và tên biến, bạn sẽ không cần thay đổi bất kỳ mã nào trong component Login
hoặc phần còn lại của component App
.
Bên trong setToken
, lưu đối số userToken
để sessionStorage
sử dụng phương thức setItem
. Phương thức này nhận một khóa làm đối số đầu tiên và một chuỗi làm đối số thứ hai. Điều đó có nghĩa là bạn sẽ cần chuyển đổi userToken
từ một đối tượng thành một chuỗi bằng cách sử dụng hàm JSON.stringify. Gọi setItem
với một khóa token
và đối tượng được chuyển đổi.
import { BrowserRouter, Route, Switch } 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">
...
</div>
);
}
export default App;
Lưu file lại. Khi bạn thực hiện, trình duyệt sẽ tải lại. Nếu bạn nhập tên người dùng và mật khẩu và gửi, trình duyệt sẽ vẫn hiển thị trang đăng nhập, nhưng nếu bạn nhìn vào bên trong các công cụ bảng điều khiển của trình duyệt, bạn sẽ thấy mã thông báo được lưu trữ trong sessionStorage
. Hình ảnh này là từ Firefox, nhưng bạn sẽ tìm thấy kết quả tương tự trong Chrome hoặc các trình duyệt hiện đại khác.
Bây giờ bạn cần truy xuất mã thông báo để hiển thị trang chính xác. Bên trong hàm getToken
, ta gọi sessionStorage.getItem
. Phương thức này nhận một đối số key
và trả về giá trị chuỗi. Chuyển đổi chuỗi thành một đối tượng bằng cách sử dụng JSON.parse, sau đó trả về giá trị của token
:
import { BrowserRouter, Route, Switch } 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');
const userToken = JSON.parse(tokenString);
return userToken?.token
}
function App() {
const token = getToken();
if(!token) {
return <Login setToken={setToken} />
}
return (
<div className="wrapper">
...
</div>
);
}
export default App;
Bạn cần sử dụng toán tử chuỗi tùy chọn— ?.
—khi truy cập thuộc tính token
vì khi bạn truy cập ứng dụng lần đầu, giá trị của sessionStorage.getItem('token')
sẽ là undefined
. Nếu bạn cố gắng truy cập một thuộc tính, bạn sẽ tạo ra lỗi.
Lưu file lại. Trong trường hợp này, bạn đã có mã thông báo được lưu trữ, vì vậy khi trình duyệt làm mới, bạn sẽ điều hướng đến các trang riêng tư:
Xóa mã thông báo bằng cách xóa mã thông báo trong tab Bộ nhớsessionStorage.clear()
trong công cụ dành cho nhà phát triển của bạn hoặc bằng cách nhập vào bảng điều khiển dành cho nhà phát triển của bạn.
Có một vấn đề nhỏ bây giờ. Khi bạn đăng nhập, trình duyệt sẽ lưu mã thông báo, nhưng bạn vẫn thấy trang đăng nhập.
Vấn đề là mã của bạn không bao giờ cảnh báo React rằng việc thu hồi mã thông báo đã thành công. Bạn vẫn cần đặt một số trạng thái sẽ kích hoạt kết xuất lại khi dữ liệu thay đổi. Giống như hầu hết các vấn đề trong React, có nhiều cách để giải quyết nó. Một trong những cách thanh lịch và có thể tái sử dụng là tạo Hook tùy chỉnh.
Tạo một mã Hook thông báo tùy chỉnh
Hook tùy chỉnh là một hàm bao bọc logic tùy chỉnh. Một Hook tùy chỉnh thường bao bọc một hoặc nhiều Hook React tích hợp sẵn cùng với các triển khai tùy chỉnh. Ưu điểm chính của Hook tùy chỉnh là bạn có thể xóa logic triển khai khỏi component và bạn có thể sử dụng lại nó trên nhiều component.
Theo quy ước, các Hook tùy chỉnh bắt đầu bằng từ khóa use*
.
Tạo một tệp mới trong thư mục App
có tên useToken.js
.
Đây sẽ là một Hook nhỏ và sẽ ổn nếu bạn định nghĩa nó trực tiếp trong App.js
. Nhưng việc di chuyển Hook tùy chỉnh sang một tệp khác sẽ cho thấy cách Hook hoạt động bên ngoài một component. Nếu bạn bắt đầu sử dụng lại Hook này trên nhiều component, bạn cũng có thể muốn chuyển nó vào một thư mục riêng biệt.
Bên trong useToken.js
, import useState
từ react
. Lưu ý rằng bạn không cần import React
vì bạn sẽ không có JSX trong tệp. Tạo và xuất một hàm được gọi là useToken
. Bên trong hàm này, hãy sử dụng Hook useState
để tạo một state token
và một hàm setToken
:
import { useState } from 'react'; export default function useToken() { const [token, setToken] = useState(); }
Tiếp theo, sao chép hàm getToken
sang Hook use
và chuyển đổi nó thành hàm mũi tên, vì bạn đã đặt nó bên trong useToken
. Bạn có thể để hàm dưới dạng một hàm tiêu chuẩn, được đặt tên, nhưng có thể dễ đọc hơn khi các hàm cấp cao nhất là chuẩn và các hàm bên trong là hàm mũi tên.
Đặt getToken
trước khai báo state, sau đó khởi tạo useState
với getToken
. Thao tác này sẽ tìm nạp mã thông báo và đặt nó làm trạng thái ban đầu:
import { useState } from 'react';
export default function useToken() {
const getToken = () => {
const tokenString = sessionStorage.getItem('token');
const userToken = JSON.parse(tokenString);
return userToken?.token
};
const [token, setToken] = useState(getToken());
}
Tiếp theo, sao chép hàm setToken
từ App.js
. Chuyển đổi thành một hàm mũi tên và đặt tên cho hàm mới là saveToken
. Ngoài việc lưu mã thông báo vào sessionStorage
, hãy lưu mã thông báo vào state bằng cách gọi setToken
:
import { useState } from 'react';
export default function useToken() {
const getToken = () => {
const tokenString = sessionStorage.getItem('token');
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);
};
}
Cuối cùng, trả về một đối tượng có chứa token
và saveToken
đặt thành tên thuộc tính setToken
. Điều này sẽ cung cấp cho component cùng một giao diện. Bạn cũng có thể trả về các giá trị dưới dạng một mảng, nhưng một đối tượng sẽ cung cấp cho người dùng cơ hội chỉ hủy cấu trúc các giá trị họ muốn nếu bạn sử dụng lại giá trị này trong một component khác.
import { useState } from 'react';
export default function useToken() {
const getToken = () => {
const tokenString = sessionStorage.getItem('token');
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
}
}
Lưu file lại.
Tiếp theo, mở App.js
ra, loại bỏ các hàm getToken
và setToken
. Sau đó import useToken
và gọi hàm hủy cấu trúc các giá trị setToken
và token
. Bạn cũng có thể xóa việc import useState
vì bạn không còn sử dụng Hook nữa:
import { BrowserRouter, Route, Switch } 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> <Switch> <Route path="/dashboard"> <Dashboard /> </Route> <Route path="/preferences"> <Preferences /> </Route> </Switch> </BrowserRouter> </div> ); } export default App;
Lưu file lại. Khi bạn làm như vậy, trình duyệt sẽ làm mới và khi bạn đăng nhập, bạn sẽ ngay lập tức chuyển đến trang. Điều này đang xảy ra bởi vì bạn đang gọi useState
trong Hook tùy chỉnh của mình, điều này sẽ kích hoạt kết xuất lại một component:
Bây giờ bạn có một Hook tùy chỉnh để lưu trữ mã thông báo của bạn vào sessionStorage
. Giờ đây, bạn có thể làm mới trang của mình và người dùng sẽ vẫn đăng nhập. Nhưng nếu bạn cố gắng mở ứng dụng trong tab khác, người dùng sẽ bị đăng xuất. sessionStorage
chỉ thuộc về phiên cửa sổ cụ thể. Mọi dữ liệu sẽ không có sẵn trong tab mới và sẽ bị mất khi đóng tab đang hoạt động. Nếu bạn muốn lưu mã thông báo trên các tab, bạn cần phải chuyển đổi sang localStorage
.
Sử dụng localStorage
để lưu dữ liệu trên Windows
Không giống như sessionStorage
, localStorage
sẽ lưu dữ liệu ngay cả sau khi phiên kết thúc. Điều này có thể thuận tiện hơn, vì nó cho phép người dùng mở nhiều cửa sổ và tab mà không cần đăng nhập mới, nhưng nó có một số vấn đề về bảo mật. Nếu người dùng chia sẻ máy tính của họ, họ sẽ vẫn đăng nhập vào ứng dụng ngay cả khi họ đóng trình duyệt. Người dùng sẽ có trách nhiệm đăng xuất một cách rõ ràng. Người dùng tiếp theo sẽ có quyền truy cập ngay vào ứng dụng mà không cần đăng nhập. Đó là một rủi ro, nhưng sự tiện lợi có thể đáng giá đối với một số ứng dụng.
Để chuyển đổi sang localStorage
, hãy mở useToken.js
, sau đó, thay đổi mọi tham chiếu của sessionStorage
thành localStorage
. Các phương thức bạn gọi sẽ giống nhau:
import { useState } from 'react'; export default function useToken() { const getToken = () => { const tokenString = localStorage.getItem('token'); 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 } }
Lưu file lại. Khi bạn làm như vậy, trình duyệt sẽ làm mới. Bạn sẽ cần đăng nhập lại vì chưa có mã thông báo nào được localStorage
đăng nhập, nhưng sau khi đăng nhập, bạn sẽ vẫn ở trạng thái đăng nhập khi mở tab mới.
Như vậy trong bước này, bạn đã lưu mã thông báo với sessionStorage
và localStorage
. Bạn cũng đã tạo một Hook tùy chỉnh để kích hoạt kết xuất lại component và di chuyển logic component sang một hàm riêng biệt. Bạn cũng đã tìm hiểu về cách thức sessionStorage
và localStorage
ảnh hưởng đến khả năng người dùng bắt đầu phiên mới mà không cần đăng nhập.
Phần kết luận
Xác thực là một yêu cầu quan trọng của nhiều ứng dụng. Sự kết hợp giữa các mối quan tâm về bảo mật và trải nghiệm người dùng có thể đáng sợ, nhưng nếu bạn tập trung vào việc xác thực dữ liệu và hiển thị các component vào đúng thời điểm, nó có thể trở thành một quy trình khá nhẹ nhàng.
Mỗi giải pháp lưu trữ cung cấp những ưu và nhược điểm riêng biệt. Lựa chọn của bạn có thể thay đổi khi ứng dụng của bạn phát triển. Bằng cách chuyển logic component của bạn vào một Hook tùy chỉnh trừu tượng, bạn có cho mình khả năng tái cấu trúc mà không làm gián đoạn các component hiện có.
Nếu bạn muốn đọc thêm các hướng dẫn về React, hãy xem lại trang Cách viết mã trong React.js.