Firebase Auth cung cấp khả năng sử dụng worker dịch vụ để phát hiện và truyền mã thông báo mã nhận dạng Firebase để quản lý phiên. Điều này mang lại các lợi ích sau:
- Có thể truyền mã thông báo nhận dạng trên mọi yêu cầu HTTP từ máy chủ mà không cần làm gì thêm.
- Có thể làm mới mã thông báo nhận dạng mà không cần thêm lượt truy cập hoặc độ trễ.
- Các phiên được đồng bộ hoá giữa phần phụ trợ và phần giao diện người dùng. Các ứng dụng cần truy cập vào các dịch vụ của Firebase như Cơ sở dữ liệu theo thời gian thực, Firestore, v.v. và một số tài nguyên bên ngoài phía máy chủ (cơ sở dữ liệu SQL, v.v.) có thể sử dụng giải pháp này. Ngoài ra, bạn cũng có thể truy cập vào cùng một phiên từ worker dịch vụ, worker web hoặc worker dùng chung.
- Không cần phải đưa mã nguồn Firebase Auth vào mỗi trang (giảm độ trễ). Worker dịch vụ, được tải và khởi chạy một lần, sẽ xử lý việc quản lý phiên cho tất cả ứng dụng ở chế độ nền.
Tổng quan
Firebase Auth được tối ưu hoá để chạy ở phía máy khách. Mã thông báo được lưu vào bộ nhớ web. Điều này giúp bạn dễ dàng tích hợp với các dịch vụ Firebase khác như Cơ sở dữ liệu theo thời gian thực, Cloud Firestore, Cloud Storage, v.v. Để quản lý các phiên từ phía máy chủ, bạn phải truy xuất và chuyển mã thông báo nhận dạng đến máy chủ.
Web
import { getAuth, getIdToken } from "firebase/auth"; const auth = getAuth(); getIdToken(auth.currentUser) .then((idToken) => { // idToken can be passed back to server. }) .catch((error) => { // Error occurred. });
Web
firebase.auth().currentUser.getIdToken() .then((idToken) => { // idToken can be passed back to server. }) .catch((error) => { // Error occurred. });
Tuy nhiên, điều này có nghĩa là một số tập lệnh phải chạy từ ứng dụng để lấy mã nhận dạng mới nhất, sau đó chuyển mã nhận dạng đó đến máy chủ thông qua tiêu đề yêu cầu, nội dung POST, v.v.
Điều này có thể không mở rộng quy mô và thay vào đó, bạn có thể cần cookie phiên phía máy chủ. Bạn có thể đặt mã thông báo nhận dạng làm cookie phiên, nhưng các cookie này có thời gian tồn tại ngắn và sẽ cần được làm mới từ ứng dụng, sau đó được đặt làm cookie mới khi hết hạn. Điều này có thể yêu cầu một lượt truy cập trả về bổ sung nếu người dùng đã không truy cập vào trang web trong một thời gian.
Mặc dù Firebase Auth cung cấp một giải pháp quản lý phiên dựa trên cookie truyền thống hơn, nhưng giải pháp này hoạt động tốt nhất cho các ứng dụng dựa trên cookie httpOnly
phía máy chủ và khó quản lý hơn vì mã thông báo ứng dụng và mã thông báo phía máy chủ có thể bị mất đồng bộ hoá, đặc biệt là nếu bạn cũng cần sử dụng các dịch vụ Firebase khác dựa trên ứng dụng.
Thay vào đó, bạn có thể sử dụng trình chạy dịch vụ để quản lý các phiên người dùng cho việc sử dụng phía máy chủ. Cách này hoạt động vì những lý do sau:
- Trình chạy dịch vụ có quyền truy cập vào trạng thái hiện tại của Firebase Auth. Bạn có thể truy xuất mã thông báo giá trị nhận dạng người dùng hiện tại từ worker dịch vụ. Nếu mã thông báo đã hết hạn, SDK ứng dụng sẽ làm mới mã thông báo đó và trả về một mã thông báo mới.
- Trình chạy dịch vụ có thể chặn và sửa đổi các yêu cầu tìm nạp.
Thay đổi về trình chạy dịch vụ
Worker dịch vụ sẽ cần bao gồm thư viện Auth và khả năng lấy mã thông báo nhận dạng hiện tại nếu người dùng đã đăng nhập.
Web
import { initializeApp } from "firebase/app"; import { getAuth, onAuthStateChanged, getIdToken } from "firebase/auth"; // Initialize the Firebase app in the service worker script. initializeApp(config); /** * Returns a promise that resolves with an ID token if available. * @return {!Promise<?string>} The promise that resolves with an ID token if * available. Otherwise, the promise resolves with null. */ const auth = getAuth(); const getIdTokenPromise = () => { return new Promise((resolve, reject) => { const unsubscribe = onAuthStateChanged(auth, (user) => { unsubscribe(); if (user) { getIdToken(user).then((idToken) => { resolve(idToken); }, (error) => { resolve(null); }); } else { resolve(null); } }); }); };
Web
// Initialize the Firebase app in the service worker script. firebase.initializeApp(config); /** * Returns a promise that resolves with an ID token if available. * @return {!Promise<?string>} The promise that resolves with an ID token if * available. Otherwise, the promise resolves with null. */ const getIdToken = () => { return new Promise((resolve, reject) => { const unsubscribe = firebase.auth().onAuthStateChanged((user) => { unsubscribe(); if (user) { user.getIdToken().then((idToken) => { resolve(idToken); }, (error) => { resolve(null); }); } else { resolve(null); } }); }); };
Tất cả yêu cầu tìm nạp đến nguồn gốc của ứng dụng sẽ bị chặn và nếu có mã thông báo nhận dạng, thì mã này sẽ được thêm vào yêu cầu thông qua tiêu đề. Phía máy chủ, tiêu đề yêu cầu sẽ được kiểm tra để tìm mã thông báo nhận dạng, xác minh và xử lý. Trong tập lệnh của trình chạy dịch vụ, yêu cầu tìm nạp sẽ bị chặn và sửa đổi.
Web
const getOriginFromUrl = (url) => { // https://stackoverflow.com/questions/1420881/how-to-extract-base-url-from-a-string-in-javascript const pathArray = url.split('/'); const protocol = pathArray[0]; const host = pathArray[2]; return protocol + '//' + host; }; // Get underlying body if available. Works for text and json bodies. const getBodyContent = (req) => { return Promise.resolve().then(() => { if (req.method !== 'GET') { if (req.headers.get('Content-Type').indexOf('json') !== -1) { return req.json() .then((json) => { return JSON.stringify(json); }); } else { return req.text(); } } }).catch((error) => { // Ignore error. }); }; self.addEventListener('fetch', (event) => { /** @type {FetchEvent} */ const evt = event; const requestProcessor = (idToken) => { let req = evt.request; let processRequestPromise = Promise.resolve(); // For same origin https requests, append idToken to header. if (self.location.origin == getOriginFromUrl(evt.request.url) && (self.location.protocol == 'https:' || self.location.hostname == 'localhost') && idToken) { // Clone headers as request headers are immutable. const headers = new Headers(); req.headers.forEach((val, key) => { headers.append(key, val); }); // Add ID token to header. headers.append('Authorization', 'Bearer ' + idToken); processRequestPromise = getBodyContent(req).then((body) => { try { req = new Request(req.url, { method: req.method, headers: headers, mode: 'same-origin', credentials: req.credentials, cache: req.cache, redirect: req.redirect, referrer: req.referrer, body, // bodyUsed: req.bodyUsed, // context: req.context }); } catch (e) { // This will fail for CORS requests. We just continue with the // fetch caching logic below and do not pass the ID token. } }); } return processRequestPromise.then(() => { return fetch(req); }); }; // Fetch the resource after checking for the ID token. // This can also be integrated with existing logic to serve cached files // in offline mode. evt.respondWith(getIdTokenPromise().then(requestProcessor, requestProcessor)); });
Web
const getOriginFromUrl = (url) => { // https://stackoverflow.com/questions/1420881/how-to-extract-base-url-from-a-string-in-javascript const pathArray = url.split('/'); const protocol = pathArray[0]; const host = pathArray[2]; return protocol + '//' + host; }; // Get underlying body if available. Works for text and json bodies. const getBodyContent = (req) => { return Promise.resolve().then(() => { if (req.method !== 'GET') { if (req.headers.get('Content-Type').indexOf('json') !== -1) { return req.json() .then((json) => { return JSON.stringify(json); }); } else { return req.text(); } } }).catch((error) => { // Ignore error. }); }; self.addEventListener('fetch', (event) => { /** @type {FetchEvent} */ const evt = event; const requestProcessor = (idToken) => { let req = evt.request; let processRequestPromise = Promise.resolve(); // For same origin https requests, append idToken to header. if (self.location.origin == getOriginFromUrl(evt.request.url) && (self.location.protocol == 'https:' || self.location.hostname == 'localhost') && idToken) { // Clone headers as request headers are immutable. const headers = new Headers(); req.headers.forEach((val, key) => { headers.append(key, val); }); // Add ID token to header. headers.append('Authorization', 'Bearer ' + idToken); processRequestPromise = getBodyContent(req).then((body) => { try { req = new Request(req.url, { method: req.method, headers: headers, mode: 'same-origin', credentials: req.credentials, cache: req.cache, redirect: req.redirect, referrer: req.referrer, body, // bodyUsed: req.bodyUsed, // context: req.context }); } catch (e) { // This will fail for CORS requests. We just continue with the // fetch caching logic below and do not pass the ID token. } }); } return processRequestPromise.then(() => { return fetch(req); }); }; // Fetch the resource after checking for the ID token. // This can also be integrated with existing logic to serve cached files // in offline mode. evt.respondWith(getIdToken().then(requestProcessor, requestProcessor)); });
Do đó, tất cả các yêu cầu đã xác thực sẽ luôn có mã nhận dạng được truyền trong tiêu đề mà không cần xử lý thêm.
Để trình chạy dịch vụ phát hiện được các thay đổi về trạng thái Xác thực, bạn phải cài đặt trình chạy dịch vụ trên trang đăng nhập/đăng ký. Đảm bảo rằng trình chạy dịch vụ được đóng gói để vẫn hoạt động sau khi trình duyệt đóng.
Sau khi cài đặt, worker của dịch vụ phải gọi clients.claim()
khi kích hoạt để có thể thiết lập làm trình điều khiển cho trang hiện tại.
Web
self.addEventListener('activate', (event) => { event.waitUntil(clients.claim()); });
Web
self.addEventListener('activate', (event) => { event.waitUntil(clients.claim()); });
Thay đổi phía máy khách
Bạn cần cài đặt trình chạy dịch vụ (nếu được hỗ trợ) trên trang đăng nhập/đăng ký phía máy khách.
Web
// Install servicerWorker if supported on sign-in/sign-up page. if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/service-worker.js', {scope: '/'}); }
Web
// Install servicerWorker if supported on sign-in/sign-up page. if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/service-worker.js', {scope: '/'}); }
Khi người dùng đăng nhập và được chuyển hướng đến một trang khác, trình chạy dịch vụ sẽ có thể chèn mã thông báo nhận dạng vào tiêu đề trước khi quá trình chuyển hướng hoàn tất.
Web
import { getAuth, signInWithEmailAndPassword } from "firebase/auth"; // Sign in screen. const auth = getAuth(); signInWithEmailAndPassword(auth, email, password) .then((result) => { // Redirect to profile page after sign-in. The service worker will detect // this and append the ID token to the header. window.location.assign('/profile'); }) .catch((error) => { // Error occurred. });
Web
// Sign in screen. firebase.auth().signInWithEmailAndPassword(email, password) .then((result) => { // Redirect to profile page after sign-in. The service worker will detect // this and append the ID token to the header. window.location.assign('/profile'); }) .catch((error) => { // Error occurred. });
Thay đổi phía máy chủ
Mã phía máy chủ sẽ có thể phát hiện mã thông báo nhận dạng trên mọi yêu cầu. Hành vi này được SDK dành cho quản trị viên hỗ trợ cho Node.js hoặc với SDK web bằng FirebaseServerApp
.
Node.js
// Server side code.
const admin = require('firebase-admin');
// The Firebase Admin SDK is used here to verify the ID token.
admin.initializeApp();
function getIdToken(req) {
// Parse the injected ID token from the request header.
const authorizationHeader = req.headers.authorization || '';
const components = authorizationHeader.split(' ');
return components.length > 1 ? components[1] : '';
}
function checkIfSignedIn(url) {
return (req, res, next) => {
if (req.url == url) {
const idToken = getIdToken(req);
// Verify the ID token using the Firebase Admin SDK.
// User already logged in. Redirect to profile page.
admin.auth().verifyIdToken(idToken).then((decodedClaims) => {
// User is authenticated, user claims can be retrieved from
// decodedClaims.
// In this sample code, authenticated users are always redirected to
// the profile page.
res.redirect('/profile');
}).catch((error) => {
next();
});
} else {
next();
}
};
}
// If a user is signed in, redirect to profile page.
app.use(checkIfSignedIn('/'));
API mô-đun web
import { initializeServerApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
export default function MyServerComponent() {
// Get relevant request headers (in Next.JS)
const authIdToken = headers().get('Authorization')?.split('Bearer ')[1];
// Initialize the FirebaseServerApp instance.
const serverApp = initializeServerApp(firebaseConfig, { authIdToken });
// Initialize Firebase Authentication using the FirebaseServerApp instance.
const auth = getAuth(serverApp);
if (auth.currentUser) {
redirect('/profile');
}
// ...
}
Kết luận
Ngoài ra, vì mã thông báo nhận dạng sẽ được đặt thông qua trình chạy dịch vụ và trình chạy dịch vụ bị hạn chế chạy từ cùng một nguồn gốc, nên không có nguy cơ xảy ra CSRF vì một trang web có nguồn gốc khác cố gắng gọi các điểm cuối của bạn sẽ không gọi được trình chạy dịch vụ, khiến yêu cầu có vẻ như không được xác thực từ quan điểm của máy chủ.
Mặc dù trình chạy dịch vụ hiện được hỗ trợ trong tất cả các trình duyệt chính hiện đại, nhưng một số trình duyệt cũ hơn lại không hỗ trợ trình chạy dịch vụ. Do đó, bạn có thể cần một số phương án dự phòng để chuyển mã nhận dạng đến máy chủ khi không có worker dịch vụ hoặc ứng dụng có thể bị hạn chế chỉ chạy trên các trình duyệt hỗ trợ worker dịch vụ.
Xin lưu ý rằng trình chạy dịch vụ chỉ có một nguồn gốc và sẽ chỉ được cài đặt trên các trang web được phân phát qua kết nối https hoặc máy chủ cục bộ.
Tìm hiểu thêm về tính năng hỗ trợ trình duyệt cho trình chạy dịch vụ tại caniuse.com.
Đường liên kết hữu ích
- Để biết thêm thông tin về cách sử dụng trình chạy dịch vụ để quản lý phiên, hãy xem mã nguồn ứng dụng mẫu trên GitHub.
- Bạn có thể xem ứng dụng mẫu đã triển khai ở trên tại https://auth-service-worker.appspot.com