使用 Service Worker 进行会话管理

Firebase 身份验证支持使用 Service Worker 检测和传递 Firebase ID 令牌以实现会话管理。这样做具有以下优势:

  • 能够在服务器的每个 HTTP 请求中传递 ID 令牌,而无需执行任何额外的操作。
  • 能够刷新 ID 令牌,而无需任何额外的往返调用,也不会造成延迟。
  • 后端和前端的会话同步。需要访问 Firebase 服务(例如实时数据库、Firestore 等)和某些外部服务器端资源(SQL 数据库等)的应用可以使用此解决方案。此外,同一会话还可以从 Service Worker、网页工作器或共享工作器访问。
  • 无需在每个页面上添加 Firebase 身份验证源代码(可减少延迟)。Service Worker 一旦加载并初始化完毕,便可在后台处理所有客户端的会话管理。

概览

Firebase 身份验证经过优化,可在客户端运行。令牌保存在网络存储中。这还可让您轻松地与其他 Firebase 服务(例如实时数据库、Cloud Firestore、Cloud Storage 等)集成。要从服务器端的角度管理会话,必须检索 ID 令牌并将其传递到服务器。

firebase.auth().currentUser.getIdToken()
  .then((idToken) => {
    // idToken can be passed back to server.
  })
  .catch((error) => {
    // Error occurred.
  });

但是,这意味着某些脚本必须从客户端运行,以获取最新的 ID 令牌,然后通过请求标头、POST 正文等将其传递给服务器。

这种方式可能无法大规模运用,并且可能需要服务器端会话 Cookie。ID 令牌可以设置为会话 Cookie,但这些 Cookie 只在短时间内有效,需要从客户端刷新,然后在到期时设置为新的 Cookie(如果用户有一段时间没有访问过网站,这可能需要额外的往返调用)。

虽然 Firebase 身份验证提供了比较传统的基于 Cookie 的会话管理解决方案,但此解决方案最适合服务器端基于 httpOnly Cookie 的应用,并且会因客户端令牌和服务器端令牌可能不同步而难以管理,尤其是在您还需要使用其他基于客户端的 Firebase 服务时。

但是,Service Worker 可用于管理用户会话以实现服务器端使用。之所以能实现这一点的原因如下:

  • Service Worker 可以访问当前的 Firebase 身份验证状态。当前用户 ID 令牌可以从 Service Worker 检索。如果此令牌过期,客户端 SDK 将刷新此令牌并返回一个新令牌。
  • Service Worker 可以拦截提取请求并加以修改。

Service Worker 发生的变化

Service Worker 将需要包含身份验证库以及获取当前 ID 令牌的功能(如果用户已登录)。

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

对应用来源的所有提取请求都将被拦截,如果 ID 令牌可用,则会通过标头附加到相应请求。系统将在服务器端检查请求标头中是否有 ID 令牌,并对其进行验证和处理。在 Service Worker 脚本中,系统将拦截并修改提取请求。

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

self.addEventListener('fetch', (event) => {
  const requestProcessor = (idToken) => {
    let req = event.request;
    // For same origin https requests, append idToken to header.
    if (self.location.origin == getOriginFromUrl(event.request.url) &&
        (self.location.protocol == 'https:' ||
         self.location.hostname == 'localhost') &&
        idToken) {
      // Clone headers as request headers are immutable.
      const headers = new Headers();
      for (let entry of req.headers.entries()) {
        headers.append(entry[0], entry[1]);
      }
      // Add ID token to header.
      headers.append('Authorization', 'Bearer ' + idToken);
      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: req.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 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.
  event.respondWith(getIdToken().then(requestProcessor, requestProcessor));
});

因此,所有经过身份验证的请求都将始终在标头中传递 ID 令牌,而无需进行其他处理。

为了让 Service Worker 能够检测到身份验证状态的变化,通常必须在登录/注册页面上安装它。安装后,Service Worker 必须在激活时调用 clients.claim(),以便可以将其设置为当前页面的控制器。

// In service worker script.
self.addEventListener('activate', event => {
  event.waitUntil(clients.claim());
});

客户端发生的变化

如果 Service Worker 受支持,需要将其安装在客户端登录/注册页面上。

// Install servicerWorker if supported on sign-in/sign-up page.
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js', {scope: '/'});
}

当用户登录并重定向到另一个页面时,Service Worker 将能够在重定向完成之前在标头中注入 ID 令牌。

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

服务器端发生的变化

服务器端代码将能够在每个请求中检测 ID 令牌。相关说明如下面的 Node.js Express 示例代码所示。

// Server side code.
const admin = require('firebase-admin');
const serviceAccount = require('path/to/serviceAccountKey.json');

// The Firebase Admin SDK is used here to verify the ID token.
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});

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

总结

此外,由于 ID 令牌将通过 Service Worker 进行设置,并且 Service Worker 被限制为从同一来源运行,因此不存在 CSRF 风险,因为尝试调用您端点的不同来源的网站将无法调用 Service Worker,从而导致服务器认为该请求未经过身份验证。

如今所有主流的新型浏览器都支持 Service Worker,但某些旧版浏览器不支持。因此,可能需要通过某种后备方式在 Service Worker 不可用时将 ID 令牌传递到服务器,或者可将应用限制为仅在支持 Service Worker 的浏览器上运行。

请注意,Service Worker 仅支持单一来源,并且将仅安装在通过 https 连接或 localhost 提供的网站上。

要详细了解 Service Worker 的浏览器支持,请访问 caniuse.com

发送以下问题的反馈:

此网页
需要帮助?请访问我们的支持页面