Xác thực và phân quyền trong kiến trúc microfrontend
Hello các bạn lại là mình đây 👋👋
Dạo này trend AI lên mạnh quá, không biết có còn bạn nào tỉ mẩn ngồi đọc blog của mình nữa không. Nhưng mà vì đam mê viết lách nên vẫn cứ lọ mọ vậy 😂
Tiếp tục với series Chập chững làm quen với Microfrontend, ở bài này chúng ta sẽ tìm hiểu cách để xác thực và phân quyền trong kiến trúc microfrontend nhé
Lên đồ rồi lên tàu với mình thoaiiiii 🛸🛸
Điều chúng ta muốn
Trước khi vô phần thực hành thì ta cùng nhau thảo luận chút những thứ mà ta đã và đang làm từ đầu series này tí coi nha.
Ở ví dụ của mỗi bài, ta luôn có phần đăng nhập, ở đó ta hardcode:
- username + password để login
- cấu hình thông tin của từng widget mà user có quyền truy cập sau khi login
const users = [
{
username: 'user',
password: 'user',
},
];
const remoteModules = [
{
remoteEntry: 'http://localhost:3001/remoteEntry.js',
remoteName: 'angular_mfe_app',
exposedModule: 'AngularAppLoader',
},
{
remoteEntry: 'http://localhost:3002/remoteEntry.js',
remoteName: 'react_mfe_app',
exposedModule: 'ReactAppLoader',
},
{
remoteEntry: 'vite:http://localhost:3003/remoteEntry.js',
remoteName: 'vue_mfe_app',
exposedModule: 'VueAppLoader',
},
];
Ý là ở đây phần xác thực (authentication) và phân quyền (authorization) là ta đang fake luôn cho tiện rồi 😂:
Khi làm vào dự án thật thì những gì ta muốn thường nom sẽ như sau:
- có 1 auth service riêng dành cho authentication và authorization
- có 1 trang manager riêng để quản lý user: CRUD user, thêm quyền (scope), và cấu hình cho từng widget (MFE)
- Khi login xong thì user sẽ chỉ load những MFE mà họ thật sự có quyền truy cập
- từ app shell khi load MFE lên cũng phải xác thực, tránh trường hợp public assets xong user khác cũng có thể truy cập "trái phép" vào các widget họ ko có quyền
- mỗi user có thể sử dụng widget bằng cách truy cập qua app-shell, hoặc mỗi widget cũng được self-host và có thể truy cập trực tiếp, có trang login riêng
Cụ thể, ta có workflow nom như sau:
Ở trên ta có:
- Ban đầu khi login thì user gọi tới Auth service để xác thực
- Sau khi xác thực xong trả về JWT token trong đó có các
scope
đại diện cho các widget mà user được quyền truy cập - sau đó mỗi khi load MFE lên thì app-shell sẽ phải
append
cái jwt token khi muốn load MFE, cụ thể là khi loadremoteEntry.js
(vì đây là file entrypoint cho từng MFE) - Ở phía mỗi MFE ta cũng có 1 đoạn verify jwt token nhỏ nhỏ để check jwt token xem có hợp lệ và có
scope
yêu cầu của từng MFE hay ko
Bên trên là workflow khi user login qua app shell, như mình có nói thì mỗi MFE cũng được self-host
và có theer được chạy trực tiếp, có trang login riêng, trông như sau:
Bên cạnh đó, ta cũng có 1 trang Manager dành cho admin để có thể quản lý users, scopes, và cấu hình của các MFE:
Ý tưởng thì là như vậy, ta vô tới phần thực hành xem đầu đuôi nó trông như thế nào nha 💪💪
Setup
Đầu tiên các bạn clone source của mình ở đây: https://github.com/maitrungduc1410/viblo-mfe-auth (branch master)
Vì bài này ta sẽ lưu thông tin vào DB nên yêu cầu các bạn phải có Docker hoặc MySQL đang chạy sẵn nhé
Sau khi clone về thì ta có như sau:
Ở trên ta để ý rằng ngoài app shell, react, vue, angular như mọi bài thì ta còn có auth-service và manager-ui
Bởi vì setup cho cả bài này tương đối mất thời gian nếu ta build từ đầu, do vậy mình đã setup tất cả cho các bạn: từ setup mysql với docker, .env
, đến các file public/private key để tạo và xác thực JWT token. Mục đích bài này là ta "cưỡi ngựa xem hoa" là chính chứ không phải mó tay vào mấy 🤣
Bước đầu tiên khi setup project là ta cài dependencies, các bạn chạy command sau ở root folder project:
npm install
Nhưng khoan hãy mở app nha, ta cần tạo db và migrate nữa. Ta sẽ chạy bằng Docker nhé, ở root folder project ta chạy:
docker compose up -d
Sau đó ta mở terminal mới ở folder auth-service
và migrate database:
npx prisma migrate deploy
Thấy như sau là oke nà:
Tiếp đó ta cần generate Typescript classes, intefaces,... cho Prisma:
npx prisma generate
Còn một bước nữa đó là ta seed database
tạo sẵn user admin, vẫn để terminal ở auth-service
ta chạy:
npm run seed
Cuối cùng là ta start project, các bạn chạy command sau ở root folder project:
npm start
Sau đó ta mở trình duyệt ở địa chỉ http://localhost:4200/login
, thấy như sau là app shell lên oke rồi nè:
Thử bấm login luôn sẽ thấy lỗi báo về từ auth-service:
Vọc vạch
Tạo mới user và phân quyền
Bây giờ vì ta mới chỉ có mỗi admin, nên ta cần phải tạo thêm các user để có thể login và truy cập vào các widget
Ta truy cập Manager UI ở địa chỉ http://localhost:4201
:
Sau đó login với username = admin@example.com
và password = admin123
:
Vô bên trong ta sẽ thấy như sau:
Hiện tại ta mới chỉ có 1 user là admin thôi, ta tạo thêm 1 user mới với username = test@gmail.com
, password = 123456
(mà các bạn chọn là gì cũng được 😂)
Tạo xong thấy như sau là oke nè:
Hiện tại ta chưa có Scope nào để gán cho test
cả, ta quay sang trang Scope nhé:
Ta bấm tạo 1 scope mới tên là ANGULAR_MFE
:
Ta chú ý rằng vì bản thân MFE cũng yêu cầu xác thực JWT và đọc scope từ JWT payload và phải match đúng với cái scope mà MFE mong muốn, với Angular MFE thì ta khai báo scope nó muốn ở angular-app/webpack.config.js
:
const SCOPE = "ANGULAR_MFE";
Với React MFE thì ở
rsbuild.config.ts
và Vue thì ởvite.config.ts
Tạo xong scope ta thấy như sau là oke nè:
Giờ ta quay lại trang Users và gán scope vào cho test
nhé, ta bấm Edit user test
rồi lưu lại:
Oke vậy là ta đã "link" user với scope rồi, ở bước cuối cùng ta sẽ tạo cấu hình MFE, và link cấu hình với các scope ta muốn
Ta mở trang MFE Config
:
Sau đó tạo mới cấu hình cho Angular MFE, các giá trị thì y hệt như trước đây ta hardcode ở file app.service.ts
của app shell:
Giá trị cụ thể như bên dưới cho bạn dễ copy paste 😘:
remoteEntry: 'http://localhost:3001/remoteEntry.js'
remoteName: 'angular_mfe_app'
exposedModule: 'AngularAppLoader'
scopes: ANGULAR_MFE
Sau đó ta lưu lại:
Ngon rồi giờ ta mở lại app shell ở địa chỉ http://localhost:4200/login
và login với user test nha:
Sau khi login xong các bạn thấy rằng user của ta chỉ có access vào Angular MFE:
Ta mở widget và app view lên test xem mọi thứ chạy oke hết không nha:
Oke chạy rồi thì ta cứ làm tới luôn đi nha 🤣🤣, ta sẽ tạo thêm 2 scope nữa cho React và Vue luôn.
Ta quay trở lại Manager UI, tạo thêm 2 scopes là REACT_MFE
và VUE_MFE
:
Sau đó quay trở lại gán tất cả scope mới cho test
:
Tiếp nữa ta sẽ tạo thêm 2 MFE config cho Vue và React:
Phần này các bạn tự nhập nha, từ đầu bài đến giờ ăn sẵn suốt rồi 🤣🤣
Âu cây vậy là mọi thứ đã setup xong ta quay lại app shell F5 coi xem sao nhé:
Ủa, sao F5 rồi mà vẫn chỉ thấy mỗi Angular MFE thôi là sao??
Lí do là vì JWT token mà ta login lúc đầu chỉ có mỗi Angular MFE, chưa được làm mới, nên khi gọi API lấy danh sahcs MFE thì chỉ trả về Angular thôi
Logic backend mình đang check cả scope trong JWT để trả về danh sách MFE, các bạn có thể thay đổi bằng cách chỉ quan tâm tới userId có trong JWT payload và luôn trả về danh sách MFE mới nhất
Và bởi vì Token này "cũ" rồi nên ta sẽ logout ra và login lại để lấy token mới. Và sau khi login lại ta sẽ thấy đầy đủ 3 widget:
Mở tất cả lên test ta sẽ thấy chạy ngon luôn:
Xác thực remoteEntry.js
Bình thường khi deploy frontend code, thường ta để nó ở public và có thể access vào thoải mái luôn
Nhưng ta đang làm xác thực MFE mà, ta sẽ không muốn 1 user có access vào mỗi Angular nhưng lại vẫn get được React MFE.
Do vậy ta sẽ thêm 1 lớp xác thực ở phía mỗi MFE, yêu cầu khi load remoteEntry.js
thì phải có token xác thực
Nếu ta mở react-app/rsbuild.config.ts
, ta sẽ thấy có đoạn:
const SCOPE = "REACT_MFE";
const publicKey = fs.readFileSync(path.join(process.cwd(), '..', "public.pem"), "utf8");
...
setupMiddlewares
...
Ở trên ta thấy rằng với React MFE, ta có cái scope mà nó cần đó là REACT_MFE
, sau tiếp đó ta load file public key
dùng để verify JWT token, file này thường sẽ được cung cấp bởi auth-service
, bên dưới là ta sẽ setupMiddlewares
để "intercept" can thiệp vào mỗi request gửi tới, check nếu là load remoteEntry.js
thì yêu cầu phải có token đi kèm, dạng:
http://localhost:3002/remoteEntry.js?token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsImlzQWRtaW4iOmZhbHNlLC....
Với kiểu này thì nếu ở app shell mà không truyền token
sang hoặc token không đủ scope
hay không hợp lệ (hết hạn,...) thì sẽ không thể load được React MFE 😘
Ta có setup tương tự cho Vue ở vite.config.ts
và Angular ở webpack.config.js
Nhìn chung để ngăn chặn load MFE khi không đủ quyền thì ta giới hạn ở file remoteEntry.js
là được, vì file đó là entrypoint cho 1 MFE, đương nhiên là ta có thể customize và thêm vào nhiều logic để check hơn ☺️
Truy cập trực tiếp MFE
Trong kiến trúc MFE, điều ta muốn là 1 MFE có thể được load từ phía App shell, nhưng nó cũng có thể được truy cập trực tiếp, có phần xác thực của riêng nó, thì nom nó mới linh hoạt, đỉnk out chứ nhờ 😂
Giờ ta truy cập React MFE xem như thế nào nhé, các bạn mở trình duyệt ở địa chỉ http://localhost:3002
ta sẽ thấy như sau:
React cũng có xác thực của riêng nó luôn! 😎, ta cũng sẽ cần phải login với user hợp lệ và có đúng scope mà React MFE mong muốn
Các bạn login với user test@gmail.com / 123456
như khi nãy ta tạo, vào bên trong ta vẫn có UI tương tự:
Nếu ta bấm Logout, sau đó login với 1 user không có REACT_MFE scope thì sẽ gặp lỗi như sau:
Lưu ý
Phần xác thực ở bài này mình làm ví dụ rất basic với JWT token, thực tế thường ta sẽ cần thêm vào nhiều logics phức tạp hơn như refresh token, hay cho phép user bình thường cũng có thể truy cập manager-ui
để config cho riêng họ, rồi nếu token hết hạn thì sẽ làm gì (force logout hay get lại token mới từ refresh token rồi load lại MFE)
Thêm nữa là nếu ta muốn support truy cập trực tiếp cho MFE thì có thể sẽ cần phải cấu hình nhiều thêm chút, ở bài này mình mới chỉ làm cho React, Vue cũng tương tự
nhưng với Angular, để setup truy cập trực tiếp thì sẽ hơi khó chút vì Angular MFE, hiện tại nếu mở Angular MFE ở địa chỉ http://localhost:3001
ta sẽ gặp lỗi sau:
Nếu ta comment phần shared
ở webpack.config.js
của angular-app
thì Angular MFE có thể được truy cập trực tiếp, nhưng lúc đó app shell lại gặp lỗi. Chỉ được 1 trong 2 😂 Và để fix cái này thì ta cần upgrade phiên bản Angular lên cao hơn để tương thích với app shell (tuyệt vời nhất là cùng phiên bản), nhưng làm như thế thì lại không "chuẩn" Microfrontend lắm khi team dev MFE không được "tự do" chọn version họ muốn 🙃
Một vấn đề nữa khi setup truy cập trực tiếp là ta cần cẩn thận nếu không có thể bundle thừa thãi nhiều hơn cần thiết và expose ở loader.ts
. Ý là phần nào ta muốn expose cho app shell load thì ta chỉ expose chính xác phần code đó thôi, tránh vì support truy cập trực tiếp mà ta lại import
thêm một phần code nào đó
Kết bài
Kể ra người đọc làm mấy bài về MFE này cũng nhàn nhỉ, không phải setup từ đầu tới cuối sẽ cực kì mất thời gian, thay vào đó chủ yếu là "cưỡi ngựa xem hoa", chỉ có mình ngồi viết bài là cực à 🤣🤣
Hi vọng rằng qua bài này đã mang tới cho các bạn một giải pháp để xác thực và phân quyền trong kiến trúc MFE, từ đó áp dụng cho sản phẩm riêng của mỗi người.
Chúc các bạn cuối tuần vui vẻ, hẹn gặp lại các bạn vào những ngày sau 👋👋
All rights reserved