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

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

  • امکان ارسال رمز شناسه روی هر درخواست HTTP از سرور بدون هیچ کار اضافی.
  • امکان به روز رسانی رمز ID بدون هیچ گونه رفت و برگشت اضافی یا تاخیر.
  • جلسات همگام سازی Backend و Frontend. برنامه هایی که نیاز به دسترسی به خدمات Firebase مانند Realtime Database، Firestore و غیره و برخی منابع جانبی سرور خارجی (پایگاه داده SQL و غیره) دارند، می توانند از این راه حل استفاده کنند. علاوه بر این، می توان به همان جلسه از سرویس کار، وب کار یا کارگر اشتراکی نیز دسترسی داشت.
  • نیاز به گنجاندن کد منبع Firebase Auth در هر صفحه را از بین می برد (تاخیر را کاهش می دهد). کارگر سرویس، یک بار بارگیری و مقداردهی اولیه می‌شود، مدیریت جلسه را برای همه مشتریان در پس‌زمینه مدیریت می‌کند.

نمای کلی

Firebase Auth برای اجرا در سمت کلاینت بهینه شده است. توکن ها در فضای ذخیره سازی وب ذخیره می شوند. این امر ادغام با سایر سرویس‌های Firebase مانند Realtime Database، Cloud Firestore، Cloud Storage و غیره را آسان می‌کند. برای مدیریت جلسات از منظر سمت سرور، نشانه‌های ID باید بازیابی شده و به سرور ارسال شوند.

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

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

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

در حالی که Firebase Auth یک راه حل سنتی مدیریت جلسه مبتنی بر کوکی ارائه می دهد، این راه حل برای برنامه های کاربردی مبتنی بر کوکی httpOnly سمت سرور بهترین کار را دارد و مدیریت آن سخت تر است زیرا نشانه های مشتری و توکن های سمت سرور ممکن است از همگام سازی خارج شوند، به خصوص اگر شما نیز نیاز به استفاده از آن داشته باشید. سایر خدمات Firebase مبتنی بر مشتری.

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

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

کارگر خدمات تغییر می کند

اگر کاربر وارد سیستم شده باشد، کارمند سرویس باید کتابخانه 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);
      }
    });
  });
};

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

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

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

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

پس از نصب، سرویس‌کار باید هنگام فعال‌سازی 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: '/'});
}

هنگامی که کاربر وارد شده و به صفحه دیگری هدایت می شود، سرویس دهنده می تواند قبل از تکمیل تغییر مسیر، کد ID را در هدر تزریق کند.

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

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

کد سمت سرور قادر خواهد بود در هر درخواست شناسه رمز را شناسایی کند. این رفتار توسط Admin SDK برای Node.js یا با Web SDK با استفاده از 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 ماژولار وب

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 وجود ندارد زیرا یک وب‌سایت با مبدا متفاوتی که تلاش می‌کند نقاط پایانی شما را فراخوانی کند، نمی‌تواند سرویس‌کار را فراخوانی کند. ، باعث می شود درخواست از دیدگاه سرور احراز هویت نشده به نظر برسد.

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

توجه داشته باشید که Service Workers فقط یک منبع هستند و فقط در وب سایت هایی که از طریق اتصال https یا لوکال هاست ارائه می شوند نصب می شوند.

در مورد پشتیبانی مرورگر برای service worker در caniuse.com بیشتر بیاموزید.