Authentication và Authorization là một phần quan trọng trong việc phát triển phần mềm, giúp chúng ta xác thực và phân quyền người dùng trước khi cho người dùng truy cập vào tài nguyên của ứng dụng. Trong bài viết này sẽ hướng dẫn các ReactJS thủ
🤣 cách implement Authentication và Authorization. A chị nào biết rồi giả bộ đọc hết bài viết rồi so sánh với cách đang dùng xem thế nào ha :))
Nẹt bô rồi gẹt gô thôi ReactJS thủ
🤣
https://react.dev/
1. Đặt vấn đề:
- Làm thế nào để redirect user về một page sau khi sign in success?
- Làm thế nào khi
reload
trang thì vẫn giữ trạng thái nếu đãauthenticated
và ngược lại? - Ví dụ web quản trị nhưng có những page chỉ quản trị cấp cao
Super Admin
mới có thể truy cập được, còn mấy ông quản trịAdmin
không được quyền truy cập. Tránh tình trạng sau này sắp bị đuổi việc mấy ông vào xoá tài liệu/ bài viết của trang web như… 😀
2. Ý tưởng
- Sử dụng
Context
làm global store quản lýuser state
, việc này cho phép toàn bộ app có thể accessuser data
. Context Provider
để wrap app, việc này hỗ trợ check phiên đăng nhập của user ngay bên trong Provider trước khi render bất kỳ page nào trong toàn app.- Bên cạnh tạo ra
Context Provider
để wrap app thì còn một số custom component để wraplayout/ page
sẽ đề cập khi triển khai.
3. Triển khai
Lưu ý: Bài viết này không đề cập đến JWT được lưu ở đâu thì bảo mật, không đề cập đến Securiry. Nên lưu ở đâu là tuỳ mấy phen hen :))
3.1. Setup
- Bài viết này sử dụng ReactJS
v18.2.0
và React Router Domv6.16.0
Do quá lười nên a chị tự initialize dự án nha,
ReactJS thủ
thì cái này quá easy ha :))
3.2. Create UI
- Trang
Sign In
src/modules/auth/sign-in/index.tsx
:
Đơn giản chỉ 1 button để gọi xuống service, bệnh lười lên ngôi nên các page khác vẫn vậy :))
- Trang
User List
src/modules/dashboard/user/list/index.tsx
:
- Trang
User Edit
src/modules/dashboard/user/edit/index.tsx
:
- Config router
src/modules/router.tsx
:
Nhớ bọc BrowserRouter
ở src/main.tsx
nha quí dị :))
return Router
component bên trong src/App.tsx
. Xong rồi đó, mà xong mỗi UI thôi, giờ thì user có thế access bất kỳ screen nào của app. Tiếp theo cần xử lý logic phần authentication và authorization.
3.3. Create Auth Context & Provider:
- Xác định state
(AuthState)
, mình cần 1 object gồm các field:isInitialized
: dùng để show loading trong qúa trình fetch user data khi người dùng reload trang.isAuthenticated
: dùng để check xem user đã đăng nhập chưa, phiên đăng nhập còn khả dụng hay không?user
: tất nhiên là user data rồi :))
src/contexts/auth/types.ts
- Xác định xong state thì
createContext
vớiinitialState
:
src/contexts/auth/AuthContext.tsx
- Xác định, khởi tạo
action
vàreducer
, như ảnh trên có định nghĩa enumAuthActionType
:INITIALIZE
– dispatch khi userreload
trang web: SetisInitialized: true
ẩn Loading sau khi fetch user data, dù có phiên đăng nhập hay không, để còn show ra trang sign in chứ k ẩn sao thấy mà sign in :)). Nếu tồn tại phiên đăng nhập thì setisAuthenticated: true
vàuser data
.SIGN_IN
– dispatch khi sign insuccess
. SetisAuthenticated: true
vàuser data
.SIGN_OUT
– dispatch khi sign out: Xoá phiên đăng nhập(token/call sign-out api,...)
Sau khi xác định được action thì khởi tạo action
và reducer
:
src/contexts/auth/reducers.ts
- Create
AuthProvider
:src/contexts/auth/AuthContext.tsx
và custom hook: src/hooks/useAuth.ts
- Nhớ bọc app lại với
AuthProvider
nha, hông là lỗi ráng chịu à :)) - Trong
AuthProvider
có 1 IIFE bên trong useEffect được thực thi 1 lần khi user reload trang, nhằm check xem có phiên đăng nhập đang tồn tại hay không bằng cáchcheck token và get profile
. Trong quá trìnhcheck phiên đăng nhập và get profile
thì sẽ showloading
nếu chưaisInitialized (false)
. Sau khi IIFE thực thi xong thì auth state đã được update tương ứng với kết quả được dispatch bên trong IIFE, khi quá trìnhINITIALIZE
hoàn tất thìisInitialized
được update thànhtrue
và ẩn đi loading –INITIALIZE function
trongreducerHandlers object
.
3.4. Sign In
- Sign in đơn giản thì mình tạo một button để call 1
sign in function
và resolve data mẫu. Sau khi Sign insuccess
thìdispatch(signIn({ user }))
. Lúc nàyauth state
đã được updateisAuthenticated: true
và cóuser data
.
Ủe toàn logic rồi tui redirect user qua lại các màn hình chỗ nào đâu? 🤣 Như này tui thấy có bọc app lại với
AuthProvider
thì user access screen nào cụng đc mà :))
Như có đề cập ngay ý tưởng ban đầu Bên cạnh tạo ra Context Provider để wrap app thì còn một số custom component để wrap layout/ page sẽ đề cập khi triển khai.
Nẹt bô gẹt gô…
3.5. “Thuê bảo vệ”
Không cho user access vô tội vạ thì mình cần “thuê bảo vệ” để canh giúp chứ ai đâu mà canh được :)). Ví dụ trong các công ty muốn ra vào cổng phải có thẻ nhân viên chẳng hạn thì bác “bảo vệ” mới cho vào cổng. Nếu có thẻ nhân viên rồi mà không có bảo vệ thì có cái thẻ cụng như không… Giống như vấn đề đang xử lý trong bài viết nếu đã sign in success
hay là load profile success
thì tiếp đến cần “bác bảo vệ”.
Guard
– Đóng vai trò như “bác bảo vệ” giúp app kiểm xoát quyền của user trước và sau xác thực, cơ bản mình sẽ tạo ra 3 guards
:
GuestGuard
bọc lấy cácscreen mà sau khi sign-in không được access
như làsign-in, sign-up
. Bên trong component này loading nếu đang trong quá trìnhINITIALIZE
vì tại thời điểm fetch data dựa vàoisAuthenticated
là không đủ để chắc chắn rằng user còn phiên đăng nhập hay không. Và đây chính là nơi mà sau khi sign-in successisAuthenticated: true
user sẽ được chuyển đếntrang được yêu cầu sign-in
, vì dùng global state, nên khi sign in success –> dispatch action –> state update –> component re-render.
src/guards/GuestGuard.tsx
- Ngược lại với
GuestGuard
,AuthGuard
bọc lấy cácscreen yêu cầu sign-in
mới có thể access. nếu user access các trang này mà chưa được xác thực(isAuthenticated: false)
thì sẽ redirect user về sign in. Nếu đã được xác thực(isAuthenticated: true)
thì render đúng trang mà nó đang bọc lấy.
src/guards/AuthGuard.tsx
RoleBasedGuard
là quá trình diễn ra sauAuthentication
, dùng để check xem user có đủ quyền truy cập vào tài nguyên app hay không, quá trình này làAuthorization
hay còn được gọi làRole Base Access Control
. Nếu user không được phép truy cập thì show warning hoặc là redirect sang trang khác (403). Trong component này nhận lấy props làaccessibleRoles
sau đó bọc lấy các page cần phân quyền, khi bọc thì chỉ định nhữngrole
nào được truy cập và page này thông quaaccessibleRoles
.
Lưu ý:
RoleBasedGuard
sẽ được bọc bên trongAuthGuard
src/guards/RoleBasedGuard.tsx
Sau khi bọc các page với guards
ta được config router:
src/modules/router.tsx
4. Tổng kết
- Cách implement với ReactJS này tách biệt việc handle sign-in với việc quản lý phiên đăng nhập của user, sign-in không cần quan tâm sau khi sign-in xong ứng dụng sẽ xảy ra hành vi gì tiếp theo, việc quản lý phiên đăng nhập cụng không cần biết sign-in đăng nhập như thế nào.
- Nếu sau này dự án cần thêm các module có yêu cầu
auth
,role base access control
thì chỉ cần wrap các module mới vớiguard
. - Là bài đầu tay của một chiếc dev non, xuất phát từ quá trình đi làm thấy nhiều mem xử lý
auth
hay bị lỗi nên mới có cảm hứng viết bài viết này, nếu có sai sót mong a/c góp ý.