مدیریت جلسه با کارگزاران خدمات

‫Firebase Auth امکان استفاده از کارگزاران سرویس را برای شناسایی و انتقال کدهای شناسایی Firebase برای مدیریت جلسه فراهم می‌کند. این کار مزایای زیر را دارد:

  • امکان ارسال یک کد شناسایی در هر درخواست HTTP از سرور بدون نیاز به کار اضافی.
  • امکان بازآوری کردن شناسه بدون هیچ‌گونه سفر رفت‌وبرگشت یا تأخیر اضافی.
  • جلسه‌های همگام‌سازی‌شده زیرینه و پیشینه. برنامه‌هایی که نیاز دارند به خدمات Firebase مثل «پایگاه داده بی‌درنگ»،‏ Firestore، و غیره و برخی‌از منابع سمت سرور خارجی (پایگاه داده SQL، و غیره) دسترسی داشته باشند می‌توانند از این راهکار استفاده کنند. علاوه‌براین، از کارگر سرویس، کارگر وب، یا کارگر هم‌رسانی‌شده نیز می‌توان به همان جلسه دسترسی داشت.
  • نیاز به افزودن کد منبع Firebase Auth در هر صفحه را ازبین می‌برد (تأخیر را کاهش می‌دهد). کارگزار سرویس که یک‌بار بارگیری و مقداردهی اولیه شده است، مدیریت جلسه را برای همه مشتریان در پس‌زمینه انجام می‌دهد.

نمای کلی

‫Firebase Auth برای اجرا در سمت کارخواه بهینه‌سازی شده است. کدها در فضای ذخیره‌سازی وب ذخیره می‌شوند. این کار ادغام با سایر سرویس‌های Firebase مانند «پایگاه داده بی‌درنگ»، Cloud Firestore،‏ Cloud Storage، و غیره را آسان می‌کند. برای مدیریت جلسه‌ها از دیدگاه سمت سرور، باید کدهای شناسایی را بازیابی و به سرور ارسال کنید.

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.
  });

بااین‌حال، این یعنی باید برخی‌از دستورگان‌ها از کارخواه اجرا شود تا جدیدترین کد شناسایی را دریافت کند و سپس آن را ازطریق سرصفحه درخواست، بدنه POST، و غیره به سرور ارسال کند.

این ممکن است مقیاس‌پذیر نباشد و درعوض ممکن است به کوکی‌های جلسه سمت سرور نیاز باشد. نشان‌های شناسایی را می‌توان به‌عنوان کوکی‌های جلسه تنظیم کرد اما این کوکی‌ها عمر کوتاهی دارند و باید از کارخواه بازآوری شوند و سپس به‌عنوان کوکی‌های جدید در انقضا تنظیم شوند که اگر کاربر مدتی از سایت بازدید نکرده باشد ممکن است به سفر رفت‌وبرگشت اضافی نیاز داشته باشد.

درحالی‌که «احراز هویت Firebase» راه‌حل مدیریت جلسه مبتنی بر کوکی سنتی‌تری ارائه می‌دهد، این راه‌حل برای برنامه‌های سمت سرور httpOnly مبتنی بر کوکی بهترین عملکرد را دارد و مدیریت آن دشوارتر است زیرا ممکن است نشان‌های کارخواه و نشان‌های سمت سرور همگام‌سازی خود را ازدست بدهند، به‌ویژه اگر نیاز باشد از دیگر سرویس‌های Firebase مبتنی بر کارخواه نیز استفاده کنید.

درعوض، می‌توان از کارگزاران سرویس برای مدیریت جلسه‌های کاربر برای مصرف سمت سرور استفاده کرد. این کار به‌دلیل موارد زیر انجام می‌شود:

  • کاربران سرویس به وضعیت فعلی Firebase Auth دسترسی دارند. رمزینه شناسه کاربر کنونی را می‌توان از کارمند سرویس بازیابی کرد. اگر نشان منقضی شده باشد، کیت توسعه نرم‌افزار کارخواه آن را بازآوری می‌کند و نشان جدیدی برمی‌گرداند.
  • کاربران سرویس می‌توانند درخواست‌های واکشی را رهگیری و آن‌ها را اصلاح کنند.

تغییرات عامل خدماتی

کارگر سرویس باید شامل کتابخانه «احراز هویت» و قابلیت دریافت شناسه فعلی باشد، درصورتی‌که کاربر به سیستم وارد شده باشد.

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);
      }
    });
  });
};

همه درخواست‌های واکشی به مبدأ برنامه رهگیری خواهد شد و اگر شناسه دردسترس باشد، ازطریق سرایند به درخواست پیوست خواهد شد. در سمت سرور، سرایند درخواست برای دریافت شناسه هویت بررسی، درستی‌سنجی، و پردازش خواهد شد. در اسکریپت عامل خدماتی، درخواست واکشی رهگیری و اصلاح می‌شود.

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));
});

در نتیجه، همه درخواست‌های اصالت‌سنجی‌شده همیشه یک کد شناسایی در سرایند بدون پردازش اضافی خواهند داشت.

برای اینکه کارگزار خدمات بتواند تغییرات وضعیت «اصالت‌سنجی» را تشخیص دهد، باید در صفحه ورود به سیستم/ثبت‌نام نصب شود. مطمئن شوید که کارمند خدمات دسته‌بندی شده باشد تا پس‌از بسته شدن مرورگر همچنان کار کند.

پس‌از نصب، کارگزار سرویس باید clients.claim() را در فعال‌سازی فراخوانی کند تا بتواند به‌عنوان کنترل‌کننده صفحه کنونی راه‌اندازی شود.

Web

self.addEventListener('activate', (event) => {
  event.waitUntil(clients.claim());
});

Web

self.addEventListener('activate', (event) => {
  event.waitUntil(clients.claim());
});

تغییرات سمت کارخواه

عامل خدماتی، درصورت پشتیبانی، باید در صفحه ورود به سیستم/ثبت‌نام سمت کارخواه نصب شود.

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: '/'});
}

وقتی کاربر به سیستم وارد می‌شود و به صفحه دیگری هدایت می‌شود، عامل خدماتی می‌تواند پیش‌از تکمیل هدایت، نشان شناسایی را در سرایند تزریق کند.

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.
  });

تغییرات سمت سرور

کد سمت سرور می‌تواند نشان شناسایی را در هر درخواست تشخیص دهد. این عملکرد توسط «کیت توسعه نرم‌افزار سرپرست» برای Node.js یا با «کیت توسعه نرم‌افزار وب» بااستفاده از 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('/'));

Web modular API

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');
    }

    // ...
}

نتیجه‌گیری

علاوه‌براین، ازآنجایی‌که نشان‌های شناسایی ازطریق کارگران سرویس تنظیم می‌شوند، و کارگران سرویس محدود به اجرا از مبدأ یکسان هستند، خطر CSRF وجود ندارد زیرا وب‌سایت با مبدأ متفاوت که تلاش می‌کند نقطه‌های پایانی شما را فراخوانی کند نمی‌تواند کارگر سرویس را فراخوانی کند و باعث می‌شود درخواست از دیدگاه سرور غیرمعتبر به‌نظر برسد.

درحالی‌که اکنون در همه مرورگرهای اصلی مدرن از کارگزاران خدمات پشتیبانی می‌شود، برخی‌از مرورگرهای قدیمی‌تر از آن‌ها پشتیبانی نمی‌کنند. درنتیجه، ممکن است به مقداری بازگشت نیاز باشد تا وقتی کارگزاران سرویس دردسترس نیستند یا وقتی برنامه می‌تواند محدود شود تا فقط در مرورگرهایی اجرا شود که از کارگزاران سرویس پشتیبانی می‌کنند، شناسه را به سرورتان منتقل کنید.

توجه داشته باشید که کارگران سرویس فقط از یک مبدأ هستند و فقط در وب‌سایت‌هایی نصب می‌شوند که ازطریق اتصال https یا localhost ارائه می‌شوند.

در caniuse.com درباره پشتیبانی مرورگر از کارگزار خدمات بیشتر بدانید.