转到控制台

使用服务工作器进行会话管理

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

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

概览

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 服务时。

作为替代,服务工作器可用于管理用户会话以实现服务器端使用。之所以能实现这一点的原因如下:

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

服务工作器发生的变化

服务工作器将需要包含身份验证库以及获取当前 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 令牌,并对其进行验证和处理。在服务工作器脚本中,系统将拦截并修改提取请求。

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 令牌,而无需进行其他处理。

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

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

客户端发生的变化

如果服务工作器受支持,需要将其安装在客户端登录/注册页面上。

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

当用户登录并重定向到另一个页面时,服务工作器将能够在重定向完成之前在标头中注入 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 令牌将通过服务工作器进行设置,并且服务工作器被限制为从同一来源运行,因此不存在 CSRF 风险,因为尝试调用您端点的不同来源的网站将无法调用服务工作器,从而导致服务器认为该请求未经过身份验证。

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

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

要详细了解服务工作器的浏览器支持,请访问 caniuse.com