如何从 firebase 函数公开缓存数据?

How to cache data publicly from firebase function?

我对 firebase 函数如何与身份验证一起工作有点迷茫,

假设我有一个函数可以提取 100 个文档并将缓存 header 设置为 24 小时。

res.set('Cache-Control', 'public, max-age=0, s-maxage=86400' // 24 * 60 * 60

默认情况下,它适用于所有用户还是按用户缓存?在某些情况下,这 100 个文档对用户来说是唯一的 - 而在其他功能中,这 100 个文档可供任何经过身份验证的用户使用。

我在文档中看到您可以设置 __session,这意味着它适用于个人用户数据,但是关于如何设置(或在何处设置)的文档并不多。是否默认设置?

我的目标是拥有一个要求用户进行身份验证的功能,然后 return 来自 non-user 特定 collection 的 100 个文档 - 也就是不必为每个用户阅读 100 个文档.但是,我认为这是不可行的,因为它需要检查每个用户是否获得授权(不可缓存)。那么有没有办法只制作一个公开可用的缓存?

非常感谢任何可以分享的信息!

Cache-Control header 用于指示用户的浏览器和任何 CDN 边缘服务器如何缓存请求。

对于需要身份验证的请求,使用 CDN 实际上是不可能的,因为您应该对这些响应使用 Cache-Control: private(Cloud Functions 的默认设置)。

虽然您可以检查您的用户是否已通过身份验证,然后将他们重定向到公共缓存资源(如 https://example.com/api/docs?sig=<somesignature>),但如果有人掌握了该 URL,该 URL 仍然可以访问 URL/cached数据。

可以说最好的方法是将您的“缓存”响应存储在单个 Cloud Firestore 文档中(如果它的大小小于 1MB 并且 JSON-compatible)或将其存储在 Cloud Storage 中。

下面包含的代码是如何使用 Cloud Firestore 缓存执行此操作的示例。我以 posts 为例,其中经过身份验证的用户是作者,但对于这个特定用例,您最好使用 Firebase SDK 进行此类查询(实时更新、更精细的控制、查询API)。类似的方法可以应用于“所有用户”资源。

如果尝试缓存 HTML 或其他一些不 JSON 友好的格式,我建议将缓存层更改为云存储。不是将 post 的数据存储在缓存条目中,而是将路径和存储桶存储到存储中的缓存文件(如下所示)。然后,如果它还没有过期,从存储和 pipe it through to the client.

中获取该文件的流
{
  data: {
    fullPath: `/_serverCache/apiCache/${uid}/posts.html`,
    bucket: "myBucket"
  },
  /* ... */
}

通用示例代码

import functions from "firebase-functions";
import { HttpsError } from "firebase-functions/lib/providers/https";
import admin from "firebase-admin";
import hash from "object-hash";

admin.initializeApp();

interface AttachmentData {
  /** May contain a URL to the resource */
  url?: string;
  /** May contain Base64 encoded data of resource */
  data?: string;
  /** Type of this resource */
  type: "image" | "video" | "social" | "web";
}

interface PostData {
  author: string;
  title: string;
  content: string;
  attachments: Record<string, AttachmentData>;
  postId: string;
}

interface CacheEntry<T = admin.firestore.DocumentData> {
  /** Time data was cached, as a Cloud Firestore Timestamp object */
  cachedAt: admin.firestore.Timestamp;
  /** Time data was cached, as a Cloud Firestore Timestamp object */
  expiresAt: admin.firestore.Timestamp;
  /** The ETag signature of the cached resource */
  eTag: string;
  /** The cached resource */
  data: T;
}

/**
 * Returns posts authored by this user as an array, from Firestore
 */
async function getLivePostsForAuthor(uid: string) {
  // fetch the data
  const posts = await admin.firestore()
      .collection('posts')
      .where('author', '==', uid)
      .limit(100)
      .get();

  // flatten the results into an array, including the post's document ID in the data
  const results: PostData[] = [];
  posts.forEach((postDoc) => {
    results.push({ postId: postDoc.id, ...postDoc.data() } as PostData);
  });
  return results;
}

/**
 * Returns posts authored by this user as an array, caching the result from Firestore
 */
async function getCachedPostsForAuthor(uid: string) {
  // Get the reference to the data's location
  const cachedPostsRef = admin.firestore()
      .doc(`_server/apiCache/${uid}/posts`) as admin.firestore.DocumentReference<CacheEntry<PostData[]>>;

  // Get the cache entry's data
  const cachedPostsSnapshot = await cachedPostsRef.get();

  if (cachedPostsSnapshot.exists) {
    // get the expiresAt property on it's own
    // this allows us to skip processing the entire document until needed
    const expiresAt = cachedPostsSnapshot.get("expiresAt") as CacheEntry["expiresAt"] | undefined;
    if (expiresAt !== undefined && expiresAt.toMillis() > Date.now() - 60000) {
      // return the entire cache entry as-is
      return cachedPostsSnapshot.data()!;
    }
  }

  // if here, the cache entry doesn't exist or has expired

  // get the live results from Firestore
  const results = await getLivePostsForAuthor(uid);

  // etag, cachedAt and expiresAt are used for the HTTP cache-related headers
  // only expiresAt is used when determining expiry

  const cacheEntry: CacheEntry<PostData[]> = {
    data: results,
    eTag: hash(results),
    cachedAt: admin.firestore.Timestamp.now(),
    // set expiry as 1 day from now
    expiresAt: admin.firestore.Timestamp.fromMillis(Date.now() + 86400000),
  };

  // save the cached data and it's metadata for future calls
  await cachedPostsRef.set(cacheEntry);

  // return the cached data
  return cacheEntry;
}

HTTPS请求函数

这是您将用于该示例中 serving Cloud Functions behind Firebase Hosting. Unfortunately the implementation details aren't as straightforward as using a Callable Function (see below) but is provided as an official project sample. You will need to insert validateFirebaseIdToken() 的请求类型,以使此代码正常工作。

import express from "express";
import cookieParserLib from "cookie-parser";
import corsLib from "cors";

interface AuthenticatedRequest extends express.Request {
  user: admin.auth.DecodedIdToken
}

const cookieParser = cookieParserLib();
const cors = corsLib({origin: true});
const app = express();

// insert from https://github.com/firebase/functions-samples/blob/2531d6d1bd6b16927acbe3ec54d40369ce7488a6/authorized-https-endpoint/functions/index.js#L26-L69
const validateFirebaseIdToken = /* ... */

app.use(cors);
app.use(cookieParser);
app.use(validateFirebaseIdToken);

app.get('/', async (req, res) => {
  // if here, user has already been validated, decoded and attached as req.user
  const user = (req as AuthenticatedRequest).user;

  try {
    const cacheEntry = await getCachedPostsForAuthor(user.uid);

    // set caching headers
    res
      .header("Cache-Control", "private")
      .header("ETag", cacheEntry.eTag)
      .header("Expires", cacheEntry.expiresAt.toDate().toUTCString());

    if (req.header("If-None-Match") === cacheEntry.eTag) {
      // cached data is the same, just return empty 304 response
      res.status(304).send();
    } else {
      // send the data back to the client as JSON
      res.json(cacheEntry.data);
    }
  } catch (err) {
    if (err instanceof HttpsError) {
      throw err;
    } else {
      throw new HttpsError("unknown", err && err.message, err);
    }
  }
});

export const getMyPosts = functions.https.onRequest(app);

可调用的 HTTPS 函数

如果您正在使用客户端 SDK,您还可以使用 Callable Functions.

请求缓存数据

这允许您像这样导出函数:

export const getMyPosts = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError(
      'failed-precondition',
      'The function must be called while authenticated.'
    );
  }

  try {
    const cacheEntry = await getCachedPostsForAuthor(context.auth.uid);
    return cacheEntry.data;
  } catch (err) {
    if (err instanceof HttpsError) {
      throw err;
    } else {
      throw new HttpsError("unknown", err && err.message, err);
    }
  }
});

并从客户端调用它:

const getMyPosts = firebase.functions().httpsCallable('getMyPosts');
getMyPosts()
  .then((postsArray) => {
    // do something
  })
  .catch((error) => {
    // handle errors
  })