我正在开发一款Gatsby应用程序,该应用程序实现了一些第三方API,所有这些都需要个人凭据。稍后,我还计划添加一个DB,因此这也需要凭据。由于我所有的代码都提交给了GitHub,我正在寻找一种安全的方式来存储和访问这些代码。
虽然我还没有部署我的应用程序,但该解决方案应该适用于本地开发和生产。
我想说的是,我过去从未(有机会)处理过类似的问题,因此我甚至不知道该去哪里寻找,也不知道该找什么来解决这个问题。
在Gatsby中区分两种类型的秘密很重要:构建时间和运行时。
- 构建时间机密用于Gatsby构建过程,例如从CMS获取数据时(您需要使用机密从CMS提取数据)
- 运行时机密在从API动态获取数据时,例如在渲染Mapbox映射时(需要将API机密传递给客户端上的Mapbox),将在客户端使用
构建时间机密
您可以将构建时间机密存储为环境变量(Gatsby文档)。
简而言之,您将创建一个包含开发环境变量的.env.development
文件,以及一个用于生产环境的.env.production
文件。
然后,在gatsby-config.js
的开头,您将添加以下内容:(无需安装dotenv
,它已经是盖茨比依赖项)
require("dotenv").config({
path: `.env.${process.env.NODE_ENV}`,
})
还要确保.env
文件在.gitignore
中,以避免将它们提交给源代码管理:
# ignore env variables files
.env*
作为最后一步,您需要手动将变量添加到您正在使用的每一项服务中,这些服务需要构建您的网站。通常,这将是您的主机(例如Netlify)和CI(例如GitHub Actions)。
运行时机密
Gatsby的优势在于在构建时获取数据并生成静态内容。如果您可以在构建时使用Gatsby源插件提取内容,那么您应该更喜欢在运行时提取内容。(有关更多详细信息,请参阅Gatsby关于构建时与运行时数据获取的文档。)
但是,在某些情况下,无法在构建时提取数据,例如,在提取动态内容或渲染Mapbox地图时。
在这种情况下,仅将机密保存为环境变量是不够的,因为这些机密仍将在网络调用中暴露。
您有几个可选选项,例如:
- 通过后端对前端(如Express.js服务器)代理API调用,并将机密添加到您的Express服务器
- 使用无服务器函数代理调用,而不必托管服务器,例如使用Netlify或AWS Lambda
Gatsby中的运行时机密显然比构建时机密更难处理。如果您的应用程序严重依赖运行时数据获取,您还可以考虑用Next.js取代Gatsby,Next.jss通过API路由很好地支持运行时机密(本质上是捆绑在框架中的无服务器功能)。
感谢HaberdashPI和JakobAttk的反馈,他们改进了这个答案
JakobAttk是对的。。。如果您不使用服务器或无服务器函数将提取的数据提供给客户端,那么每个人(只了解一些开发人员)都可以看到API-Keys。这很容易,就像1,2,3…你只需要在DEV工具中打开你的网络选项卡,然后寻找xhr连接。从API获取数据,并检查请求发送的头的值。
如果您不想(而且永远不应该!!!)将敏感数据放在git存储库中,ENV变量是可以的,但它们在运行时并没有完全隐藏。
至少,到api-service的最后一英里保留了它的凭据,所以永远不要对敏感的api密钥使用不安全的客户端连接。如果您的API-服务有额外的安全层(例如检查referer IP),这可能没问题。
我个人不喜欢在.env
文件中存储机密的想法,所以现在我通常会使用类似AWS secrets Manager的东西。您可以使用AWS SDK轻松设置秘密存储和获取密钥。当然,这一切都假设你在某个时候使用AWS,但我喜欢这样做,因为我可以控制哪些角色可以访问我的密钥——所以如果你正在建立一个构建管道,这可能是一个很好的方法。在构建时,我可能会做这样的
import AWS from "aws-sdk";
const region = "us-east-1";
const creds = new AWS.SharedIniFileCredentials({ profile: "some-profile-name" });
AWS.config.credentials = creds;
export class SecretsManagerService {
private static _client: AWS.SecretsManager;
static get client(): AWS.SecretsManager {
if (!SecretsManagerService._client) {
SecretsManagerService._client = new AWS.SecretsManager({
region,
});
}
return SecretsManagerService._client;
}
static async getSecretValue(secretId: string): Promise<string> {
try {
const secret = await SecretsManagerService.client
.getSecretValue({ SecretId: secretId })
.promise();
return secret.SecretString!;
} catch (e) {
throw e;
}
}
}
而且,这一切都很好,除了你需要使用盖茨比的秘密,所以我做了这样的事情(假设我使用Contentful作为我的CMS)
import { SecretsManagerService } from "./aws-secrets.service";
import { execSync } from 'child_process';
(async () => {
const { CONTENTFUL_ENV } = process.env;
const secretPath = CONTENTFUL_ENV === 'qa' ? 'qa-secret-path' : 'prod-secret-path';
const values = await SecretsManagerService.getSecretValue(secretPath);
const parsed = JSON.parse(values);
process.env.CONTENTFUL_KEY = parsed.contentfulSecretKey;
const command = CONTENTFUL_ENV === 'qa' ? 'gatsby develop' : 'gatsby build';
execSync(command, { stdio: 'inherit'});
})();
通过这种方式,我可以像这样在Gatsby配置中继承我的环境变量(再次假设我使用Contentful作为CMS)。
{
resolve: "gatsby-source-contentful",
options: {
accessToken: process.env.CONTENTFUL_KEY,
environment: process.env.CONTENTFUL_ENV,
spaceId: "my-space-id",
},
},
并将你的package.json脚本修改为这样的
"scripts": {
"develop": "CONTENTFUL_ENV=qa ts-node ./tools/index.ts",
"start": "gatsby develop",
"build": "CONTENTFUL_ENV=master ts-node ./tools/index.ts",
"serve": "gatsby serve",
"clean": "gatsby clean"
},