如何从 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
})
我对 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
})