去年,就有一位客户打电话向我咨询过 exactly 这个问题。有人出于直觉运行了 git log -p 命令,结果发现两年前有一份 .env 文件被提交到了代码库中,但此前这一错误根本没有被发现。数据库密码、Stripe 的密钥以及 JWT 签名密钥——所有这些敏感信息仍然处于活跃使用状态,而且仍在生产环境中被使用。
根据 IBM 在 2024 年发布的报告,平均来说,一次数据泄露造成的损失为 488万美元 —— 这只是平均值,并非最严重的情况。
导致数据泄露的根本原因往往与敏感信息的泄露有关。仅在 2023 年一年,GitHub 就发现有超过一百万条敏感信息在公共代码库中被泄露了;更何况那些从未被任何人发现的私有敏感信息更是数不胜数。
这并不是开发人员的问题。与我合作过的那些开发者们都非常谨慎,但现有的系统架构本身就容易导致他们犯错:有时候 .env 文件会不小心被提交到代码库中;有时人们会将敏感信息复制并粘贴到 Slack 消息中,以便帮助同事解除账号锁定;有时在发布 Docker 镜像时,这些敏感信息也会被包含在其中;还有时候服务器被关闭后,却没有人去更换它之前保存的敏感信息。
Azure Key Vault 的解决方案截然不同。你的应用程序可以在运行时从中央化的、加密存储的服务中获取所需的敏感信息,因此 .env 文件就不再会成为安全隐患了,因为其中已经不再包含任何值得被窃取的信息。
你所要构建的是一个 Node.js Express API,它在程序启动时会从 Azure Key Vault 中获取所有必要的敏感信息。代码中根本不会包含任何密码。当有员工离职时,代码库中也不存在需要更换的敏感信息;最终 .env 文件里只会留下一行内容——也就是 Key Vault 的名称而已。
先决条件
-
Node.js 18 及更高版本
-
一个 Azure 账户(免费账户即可)
- 已安装并登录了 Azure CLI (
az login) - 具备 Express.js 的基础知识
- Docker(可选——仅用于本地数据库测试环节)
我们要构建什么
我们将构建一个 Node.js Express API,该 API会:
-
在程序启动时,从 Azure Key Vault 中获取所需的认证信息,然后使用这些信息连接到 PostgreSQL 数据库
-
采用“托管身份”机制进行身份验证——因此代码中根本不需要任何客户端密钥或密码
-
将敏感信息缓存到内存中,这样每次请求时就不需要再次从 Key Vault 中获取这些信息了
-
可以通过 Azure CLI 在本地进行测试,而在生产环境中则可以使用“托管身份”机制——代码完全相同,无需做任何修改
目录结构
架构的工作原理
在编写任何代码之前,先了解整个系统的运作流程会很有帮助:
本地开发环境
.-------------------------------------------------------.
| |
| [Node.js应用程序] |
| | |
| v |
| [DefaultAzureCredential] ---> 访问Azure登录会话 |
| | |
| v |
| [Azure Key Vault] --->> 提供加密密钥 |
| | |
| v |
| [内存缓存] --->> 应用在运行时使用这些密钥 |
'-------------------------------------------------------'
生产环境(Azure)
.-------------------------------------------------------.
| |
| [Azure应用服务] |
| | |
| v |
| [DefaultAzureCredential] --->> 使用托管身份认证 |
| | |
| v |
| [Azure Key Vault] --->> 提供加密密钥 |
| | |
| v |
| [内存缓存] --->> 应用在运行时使用这些密钥 |
'-------------------------------------------------------'
这两种环境运行的代码是完全相同的。DefaultAzureCredential会自动确定应该从哪里获取加密信息:在本地环境中,它会使用你的Azure登录会话;而在Azure生产环境中,则会使用托管身份认证机制。你无需手动切换配置文件或管理凭证信息,系统就能正常运行。
什么是Azure Key Vault?
Azure Key Vault是微软提供的加密密钥管理系统,它可以用来存储各种敏感信息,如数据库密码、API密钥以及JWT签名密钥等。在本教程中,我们主要关注它的加密密钥存储功能——那些应用程序运行所必需的信息,但又不适合被保存在Git代码库中的数据。
与传统的`.env`文件相比,在实际开发过程中,使用Azure Key Vault确实能带来很多便利。因此,在开始编写代码之前,了解这两者之间的区别是非常重要的。
在真实项目中,我注意到密钥轮换机制非常实用。当你在Azure Key Vault中更新了某个加密密钥后,所有应用程序在下次重启时都会自动使用新版本的密钥,这样就无需在测试环境和生产环境中分别查找不同的配置文件了。
访问控制也是另一个非常重要的功能。每个应用程序只能被允许读取它真正需要的加密信息。这样一来,即使某个服务遭到攻击,也无法获取其他服务的敏感数据。
每一次读取操作都会被记录下来。当出现问题时——而最终总会出现问题的——你可以清楚地看到是哪个应用程序在什么时间访问了哪条保密信息。审计人员恰恰想要看到的就是这样的记录。
我参加过很多安全审查,因此知道“我们使用.env文件,并要求员工不要将这些文件提交到代码库中”这样的做法是无法让审核人员满意的。SOC 2、HIPAA、GDPR这些标准都要求提供可证明的有效控制措施,而一个带有访问日志的密钥保管库恰恰能够满足这一要求。
设置密钥保管库
请运行以下命令。密钥保管库的名称必须在整个Azure平台上保持唯一性——而不能只是在你自己的订阅账户中唯一,因此请选择一个具体的名称。名称应由字母、数字和连字符组成,长度为3到24个字符。
# 创建一个资源组(如果你已经有了这个资源组,就可以跳过这一步)
az group create \
--name keyvault-demo-rg \
--location eastus
# 创建密钥保管库(默认情况下会启用RBAC权限控制——后续的角色分配步骤需要这一设置)
az keyvault create \
--name your-vault-name \
--resource-group keyvault-demo-rg \
--location eastus
# 授予自己管理这些秘密的权限(在启用RBAC的情况下这是必需的——创建密钥保管库的用户并不会自动被赋予这些权限)
az role assignment create \
--role "Key Vault Secrets Officer" \
--assignee-object-id $(az ad signed-in-user show --query id -o tsv) \
--scope $(az keyvault show \
--name your-vault-name \
--resource-group keyvault-demo-rg \
--query id -o tsv)
# 添加你需要保护的秘密信息
az keyvault secret set \
--vault-name your-vault-name \
--name "DB-HOST" \
--value "your-db-host.postgres.database.azure.com"
az keyvault secret set \
--vault-name your-vault-name \
--name "DB-PASSWORD" \
--value "your-super-secret-password"
az keyvault secret set \
--vault-name your-vault-name \
--name "JWT-SECRET" \
--value "your-jwt-signing-secret"
请验证这些秘密信息是否已经被成功存储到密钥保管库中:
az keyvault secret list --vault-name your-vault-name --query "[].name" -o tsv
你应该会看到如下结果:
DB-HOST
DB-PASSWORD
JWT-SECRET
创建Node.js项目
首先设置项目的结构:
mkdir nodejs-azure-keyvault
cd nodejs-azure-keyvault
npm init -y
npm install express pg jsonwebtoken @azure/keyvault-secrets @azure/identity dotenv
其中两个Azure相关的包会完成所有的核心功能:
-
@azure/keyvault-secrets——用于连接你的密钥保管库并提取其中的秘密信息。 -
@azure/identity——负责处理身份验证相关的工作。在本地开发环境中,它会使用你的az login会话进行认证;而在生产环境中,则会自动切换到“托管身份识别”服务。
在package.json文件中添加一个启动脚本:
npm pkg set scripts.start="node server.js"
最后创建如下文件结构:
nodejs-azure-keyvault/
|-- src/
| |-- config/
| | `-- secrets.js # 用于处理与密钥保管库相关的逻辑
| |-- db/
| | `-- index.js # 使用从密钥保管库中获取的秘密信息来操作PostgreSQL数据库
| `-- routes/
| `-- users.js # 示例路由处理函数
|-- app.js # Express应用程序的主文件
`-- server.js # 程序的入口点文件——会首先加载秘密信息
使用“管理身份”功能连接Key Vault
创建密钥配置文件:
// src/config/secrets.js
const { SecretClient } = require('@azure/keyvault-secrets');
const { DefaultAzureCredential } = require '@azure/identity';
const VAULT_URL = `https://${process.env.KEY_VAULT_NAME}.vault.azure.net`;
const credential = new DefaultAzureCredential();
const client = new SecretClient(VAULT_URL, credential);
async function getSecret(name) {
const secret = await client.getSecret(name);
return secret.value;
}
module.exports = { getSecret };
DefaultAzureCredential是此配置中最重要的部分。它会按顺序尝试一系列认证方法:
-
环境变量(用于CI/CD管道)
-
Azure CLI凭据(用于本地开发——使用
az login命令) -
“管理身份”功能(用于在Azure上部署的应用程序)
这意味着,无论是在本地环境还是生产环境中,使用完全相同的代码即可正常运行,且无需进行任何修改。在本地环境中,系统会使用你的az login会话信息;而在生产环境中,则会使用应用程序的“管理身份”功能。你根本不需要手动处理任何凭据相关事宜。
在程序启动时缓存密钥
每次请求时都调用Key Vault会导致延迟并产生费用。因此,可以在程序启动时一次性加载所有密钥,并将它们缓存在内存中。请用以下完整代码替换src/config/secrets.js文件:
// src/config/secrets.js const { SecretClient } = require('@azure/keyvault-secrets'); const { DefaultAzureCredential } = require '@azure/identity'; const VAULT_URL = `https://${process.env.KEY_VAULT_NAME}.vault.azure.net`; const credential = new DefaultAzureCredential(); const client = new SecretClient(VAULT_URL, credential); // 内存缓存 const cache = {}; async function getSecret(name) { if (cache[name]) return cache[name]; const secret = await client.getSecret(name); cache[name] = secret.value; return secret.value; } async function loadAllSecrets() { console.log('正在从Azure Key Vault加载密钥...const secretNames = ['DB-HOST', 'DB-PASSWORD', 'JWT-SECRET']; await Promise.all( secretNames.map(async (name) => { cache[name] = await getSecret(name); console.log(` ✓ 已成功加载${name}`); }) ); console.log('所有密钥均已成功加载。'); } function getFromCache(name) { if (!cache[name]) throw new Error(`未加载密钥“${name}”。是否执行过loadAllSecrets()函数?`); return cache[name]; } module.exports = { loadAllSecrets, getFromCache };
loadAllSecrets函数会在应用程序启动时被调用一次。之后,所有密钥都会从内存缓存中直接提供出来,从而完全避免再次调用Key Vault接口,进而降低延迟并节省成本。
在Express API中使用密钥
使用缓存的密钥来建立数据库连接:
// src/db/index.js
const { Pool } = require('pg');
const { getFromCache } = require('../config/secrets');
let pool;
function getPool() {
if (!pool) {
pool = new Pool({
host: getFromCache('DB-HOST'),
database: process.env.DB_NAME || 'myapp',
user: process.env.DB_USER || 'dbadmin',
password: getFromCache('DB-PASSWORD'),
port: parseInt(process.env.DB_PORT || '5432'),
ssl: process.env.NODE_ENV === 'production'
? { rejectUnauthorized: false }
: false,
});
pool.on('error', (err) => {
console.error('数据库池出现异常错误:', err.message);
});
}
return pool;
}
module.exports = { getPool };
请注意其中的区别:DB-HOST和DB-PASSWORD这些信息属于敏感数据,因此是从Key Vault中获取的;而数据库名称、用户名以及端口号则不属于敏感信息,它们不需要受到保护,因此使用环境变量,并设置了合理的默认值。Key Vault主要用于存储凭证信息,而不是所有的配置参数。
SSL相关的设置会根据运行环境进行自动调整:在生产环境中会强制启用SSL加密;而在本地开发环境中则会关闭SSL加密,这样Docker连接就可以在不使用证书的情况下正常工作。rejectUnauthorized: false这个设置允许Azure Database for PostgreSQL使用其自带的证书,而无需验证证书链——这对于由Azure管理的数据库来说是常见的做法。在要求更严格的安全环境下,你可以下载Azure的根CA证书,并通过pool配置中的ca选项来指定该证书。
下面是一个示例代码,展示了如何使用从Key Vault中获取的密钥来进行JWT验证:
// src/routes/users.js
const express = require('express');
const jwt = require('jsonwebtoken');
const { getFromCache } = require('../config/secrets');
const { getPool } = require('../db');
const router = express.Router();
// 认证中间件——JWT密钥来自Key Vault,而不是环境变量process.env
function authMiddleware(req, res, next) {
const authHeader = req.headersauthorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authorization header缺失或格式不正确' });
}
const token = authHeader.split(' ')[1];
try {
req.user = jwt.verify(token, getFromCache('JWT-SECRET'));
next();
} catch (err) {
return res.status(401).json({ error: '令牌无效或已过期' });
}
}
// GET /api/users — 显示所有用户信息(需要认证)
router.get('/', authMiddleware, async (req, res) => {
try {
const result = await getPool().query(
'SELECT id, email, created_at FROM users ORDER BY created_at DESC LIMIT 20'
);
res.json(result.rows);
} catch (err) {
console.error('数据库出现错误:', err.message);
res.status(500).json({ error: '内部服务器错误' });
}
});
// GET /api/users/:id — 显示指定用户的详细信息(需要认证)
router.get('/:id', authMiddleware, async (req, res) => {
try {
const result = await getPool().query(
'SELECT id, email, created_at FROM users WHERE id = $1',
[req.params.id]
);
if (!result.rows[0]) return res.status(404).json({ error: '用户未找到' });
res.json(result.rows[0]);
} catch (err) {
console.error('数据库出现错误:', err.message);
res.status(500).json({ error: '内部服务器错误' });
}
});
module.exports = router;
请注意,错误处理程序返回的是 `'内部服务器错误'`,而不是 `err.message`。数据库错误会泄露很多敏感信息——如果让这些错误通过的话,攻击者就能获取到你的表名、列名以及查询结构。
配置 Express 应用程序时,这两个文件中都定义了 `authMiddleware` 中间件——是的,这个中间件被重复定义了。在生产环境中,我会将这个中间件放在一个共享的中间件文件中。但对于本教程来说,将其保留在本地文件中意味着你可以直接阅读其中任何一个文件,而无需再去查找其他三个文件。
“`javascript
// app.js
const express = require(‘express’);
const jwt = require(‘jsonwebtoken’);
const { getFromCache } = require(‘./src/config/secrets’);
const usersRouter = require(‘./src/routes/users’);
const app = express();
app.use(express.json());
// 认证中间件——JWT 密钥来自 Key Vault,而不是 process.env
function authMiddleware(req, res, next) {
const authHeader = req.headersauthorization;
if (!authHeader || !authHeader.startsWith(‘Bearer ‘)) {
return res.status(401).json({ error: ‘缺少或格式错误的 Authorization 头部’ });
}
const token = authHeader.split(‘ ‘)[1];
try {
req.user = jwt.verify(token, getFromCache(‘JWT-SECRET’));
next();
} catch (err) {
return res.status(401).json({ error: ‘无效或过期的令牌’ });
}
}
// 健康检查——不需要进行认证
app.get(‘/health’, (req, res) => {
res.json({ status: ‘healthy’, timestamp: new Date().toISOString() });
});
// 状态端点——无需数据库即可验证 Key Vault 的集成情况
app.get(‘/api/status’, authMiddleware, (req, res) => {
res.json({
message: ‘所有密钥已从 Azure Key Vault 中加载完毕’,
vault: process.env.KEY_VAULT_NAME,
secrets_loaded: [‘DB-HOST’, ‘DB-PASSWORD’, ‘JWT-SECRET’],
authenticated_as: req.user.email,
timestamp: new Date().toISOString()
});
});
app.use(‘/api/users’, usersRouter);
app.use((req, res) => res.status(404).json({ error: ‘未找到路由’ }));
app.use((err, req, res, next) => {
console.error(‘发生未知错误:’, err.message);
res.status(500).json({ error: ‘内部服务器错误’ });
});
module.exports = app;
“`
在启动服务器之前,入口脚本会先加载所有的密钥。只有当所有密钥都成功加载后,服务器才会真正开始运行。
“`javascript
// server.js
require(‘dotenv’).config();
const app = require(‘./app’);
const { loadAllSecrets } = require(‘./src/config/secrets’);
const PORT = process.env.PORT || 3000;
async function start() {
try {
await loadAllSecrets();
app.listen(PORT, () => {
console.log(`服务器正在端口 ${PORT} 上运行`);
});
} catch (err) {
console.error(‘无法启动服务器:’, err.message);
console.error(‘提示:在本地开发时请运行 “az login” 命令;在 Azure 环境中请使用 Managed Identity 功能。’);
process.exit(1);
}
}
start();
“`
之所以使用 `process.exit(1)`,是因为我更愿意让应用程序在启动时就直接崩溃,而不是带着缺失的凭据继续运行,然后在两小时后收到第一个请求时才出现故障。
在本地进行测试
为本地开发创建一个`.env`文件。该文件中仅包含Key Vault的名称,不包含任何敏感信息:
# .env
KEY_VAULT_NAME=你的-vault-name
PORT=3000
将`.env`文件以及部署用的zip文件添加到`.gitignore`中:
echo ".env" >> .gitignore
echo "app.zip" >> .gitignore
确保你已经登录到了Azure CLI:
az login
启动应用程序:
npm start
你应该会看到如下输出:
正在从Azure Key Vault中加载密钥信息…
✓ JWT密钥已成功加载
✓ 数据库密码已成功加载
✓ 数据库主机地址已成功加载
所有密钥信息均已成功加载。
服务器正在3000端口运行
密钥信息的加载顺序可能会有所不同——`Promise.all`会并行获取这些信息,并在每个操作完成后再进行合并处理。关键的是,在服务器启动之前,必须确保这三个密钥信息都已被正确加载。
测试健康检查端点:
curl http://localhost:3000/health
# 返回结果:{"status":"healthy","timestamp":"2026-07-14T19:38:11.659Z"}
现在,我们来验证整个集成流程是否正常工作。取出你之前存储为`JWT-SECRET`的密钥值,用它来生成一个测试令牌——请将这个令牌值替换为`YOUR-JWT-SECRET-VALUE`。然后使用这个令牌访问 `/api/status` 端点:
node -e "const jwt = require('jsonwebtoken'); console.log(jwt.sign({id:1, email:'test@test.com'}, 'YOUR-JWT-SECRET-VALUE', {expiresIn:'1h'}));"
在Linux/macOS系统中:
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/api/status
在Windows PowerShell中:
Invoke-RestMethod -Uri "http://localhost:3000/api/status" -Headers @{Authorization = "Bearer YOUR_TOKEN"}
你应该会看到如下响应:
{
"message": "所有密钥信息均已从Azure Key Vault中成功加载",
"vault": "your-vault-name",
"secrets_loaded": ["DB-HOST", "DB-PASSWORD", "JWT-SECRET"],
"authenticated_as": "test@test.com",
"timestamp": "2026-07-14T19:50:08.687Z"
}
如果收到了这样的响应,那就说明整个集成流程是正常的。JWT令牌的生成和验证过程所使用的密钥信息仅存储在Key Vault中,既不在你的代码中,也不在`.env`文件里,更不会出现在代码仓库中的任何地方。你的`az login`会话负责处理本地的身份认证逻辑;而在生产环境中,系统会使用“托管身份”功能来替代这一过程。代码本身是不变的。
使用Docker测试完整的数据库流程
该应用程序会从Key Vault中读取`DB-HOST`和`DB-PASSWORD`这些密钥信息,因此这些值需要与你在本地Docker容器中设置的值保持一致。现在请更新这些值:
az keyvault secret set --vault-name your-vault-name --name "DB-HOST" --value "localhost"
az keyvault secret set --vault-name your-vault-name --name "DB-PASSWORD" --value "demopassword123"
Docker会创建一个Postgres容器。密码必须与你在Key Vault中设置的值〈code>demopassword123
docker run --name pg-demo \
-e POSTGRES_USER=dbadmin \
-e POSTGRES_PASSWORD=demopassword123 \
-e POSTGRES_DB=myapp \
-p 5432:5432 \
-d postgres:15
创建相应的表并插入一些测试数据:
docker exec -it pg-demo psql -U dbadmin -d myapp -c \
"CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW());"
docker exec -it pg-demo psql -U dbadmin -d myapp -c \
"INSERT INTO users (email) VALUES ('alice@example.com'), ('bob@example.com'), ('carol@example.com');"
关闭服务器后再重新启动它——因为配置信息是在服务器启动时加载的,所以需要重新运行一次才能应用你在Key Vault中所做的更改:
npm start
使用有效的JWT调用用户信息接口:
这个地址。# 生成一个token(使用你在Key Vault中存储的JWT-SECRET值)
node -e "const jwt = require('jsonwebtoken'); console.log(jwt.sign({id:1, email:'test@test.com'}, 'YOUR-JWT-SECRET-VALUE', {expiresIn:'1h'}));"
在Linux/macOS系统中:
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/api/users
在Windows PowerShell中:
Invoke-RestMethod -Uri "http://localhost:3000/api/users" -Headers @{Authorization = "Bearer YOUR_TOKEN"}
你应该会看到如下结果:
[
{ "id": 1, "email": "alice@example.com", "created_at": "2026-07-14T19:59:21.064Z" },
{ "id": 2, "email": "bob@example.com", "created_at": "2026-07-14T19:59:21.064Z" },
{ "id": 3, "email": "carol@example.com", "created_at": "2026-07-14T19:59:21.064Z" }
]
这个查询是使用直接从Key Vault中获取的密码来执行的。该密码既不在你的〈code>.env文件中,也没有被硬编码在任何地方,更不会存储在本地变量中。因此,这个代码仓库本身并没有任何值得窃取的信息。
在部署之前,请将真正的生产环境配置信息重新保存到Key Vault中:
az keyvault secret set --vault-name your-vault-name --name "DB-HOST" --value "your-db-host.postgres.database.azure.com"
az keyvault secret set --vault-name your-vault-name --name "DB-PASSWORD" --value "your-super-secret-password"
如果跳过这一步,部署后的应用程序会尝试连接到〈code>localhost,但会立即失败——因为在App Service环境中并不存在〈code>localhost
部署到Azure App Service
注意:这一部分主要是用于创建App Service所需的基础设施。实际的代码部署(即将压缩文件上传到服务器)会在下一节中进行。在应用程序首次启动之前,必须确保它已经配置好了对Key Vault的访问权限,否则它会立即失败并退出运行。
创建应用服务:
# 创建一个应用服务计划(B1是最便宜的付费等级)
az appservice plan create \
--name keyvault-demo-plan \
--resource-group keyvault-demo-rg \
--sku B1 \
--is-linux
# 创建Web应用
az webapp create \
--name my-keyvault-node-app \
--resource-group keyvault-demo-rg \
--plan keyvault-demo-plan \
--runtime "NODE:18-lts"
# 设置应用配置——KEY_VAULT_NAME用于指定应用应使用哪个密钥库
# NODE_ENV=production可启用数据库连接的SSL加密
az webapp config appsettings set \
--name my-keyvault-node-app \
--resource-group keyvault-demo-rg \
--settings KEY_VAULT_NAME=your-vault-name NODE_ENV=production
为应用授予Key Vault访问权限
为该应用启用“托管身份”功能。这样,该应用就能在Microsoft Entra ID系统中获得一个Key Vault可以信任的身份:
# 启用系统分配的托管身份
az webapp identity assign \
--name my-keyvault-node-app \
--resource-group keyvault-demo-rg
以下命令会自动获取principalId,并使用它来为应用授予相应的权限:
# 获取principal ID
PRINCIPAL_ID=$(az webapp identity show \
--name my-keyvault-node-app \
--resource-group keyvault-demo-rg \
--query principalId \
--output tsv)
# 获取Key Vault的资源ID
KV_ID=$(az keyvault show \
--name your-vault-name \
--resource-group keyvault-demo-rg \
--query id \
--output tsv)
# 为应用授予“Key Vault Secrets User”角色
az role assignment create \
--role "Key Vault Secrets User" \
--assignee-object-id $PRINCIPAL_ID \
--scope $KV_ID
“Key Vault Secrets User”角色仅允许应用读取密钥信息,而不允许其创建、更新或删除这些密钥。这体现了最小权限原则——应用程序只能执行它真正需要的操作。
现在可以部署该应用了。Linux/macOS用户可以直接运行相关命令;Windows用户则需要打开Git Bash(Git for Windows自带此工具):
zip -r app.zip . -x "node_modules/*" ".git/*" ".env" "app.zip"
然后进行部署:
az webapp deployment source config-zip \
--name my-keyvault-node-app \
--resource-group keyvault-demo-rg \
--src app.zip
部署完成后,该应用会自动使用其“托管身份”功能与Key Vault进行交互。在整个部署过程中,既不需要密码,也不需要任何客户端密钥或认证信息。
可以通过检查健康检查端点来确认应用是否正常运行:
curl https://my-keyvault-node-app.azurewebsites.net/health
# 返回结果类似 {"status":"healthy","timestamp":"..."}
如果应用无法启动,可以查看日志信息:
az webapp log tail --name my-keyvault-node-app --resource-group keyvault-demo-rg
在绝大多数情况下,问题都是由于Key Vault角色的授权尚未生效。请等待2–3分钟后再尝试重启应用。
az webapp restart --name my-keyvault-node-app --resource-group keyvault-demo-rg
无需重新部署即可更换密钥
Key Vault最大的实际优势之一就是能够实现密钥的及时更新。当需要更改数据库密码时,你只需在Key Vault中更新该密码,而无需修改应用程序代码:
az keyvault secret set \
--vault-name your-vault-name \
--name "DB-PASSWORD" \
--value "new-rotated-password"
系统会在启动时自动加载这些密钥,因此无需重新部署应用程序,只需重启即可:
az webapp restart \
--name my-keyvault-node-app \
--resource-group keyvault-demo-rg
整个过程中不需要修改任何代码,也不需要进行新的部署。密钥更新后,应用程序会在几秒钟内立即使用新密码。
如果你需要实现无中断的密钥更新功能,可以在管理员身份认证之后添加一个/refresh-secrets端点,该端点会清除缓存,然后调用loadAllSecrets()函数。顺序非常重要——loadAllSecrets()函数会使用getSecret()函数来获取密钥值,而如果缓存中存在这些值,getSecret()函数就会直接返回缓存中的内容。因此,你必须先清除缓存,这样才能确保新密码能够被正确加载。这个功能是可选的,但对于那些无法承受应用程序重启的长周期运行进程来说,它非常有用。
故障排除
CredentialUnavailableError: DefaultAzureCredential无法获取令牌
这是因为你还没有登录到Azure CLI。请运行az login后再试一次。在Azure App Service中,需要确认“管理身份”功能已启用,并且角色分配操作已经正确完成。
RestError: 禁止访问——该用户没有获取密钥的权限
这可能是因为“管理身份”功能尚未与Key Vault连接成功。请重新运行az role assignment create命令。如果你已经执行过这个操作,那么可能需要等待一段时间才能看到效果。Azure系统通常需要2到3分钟的时间来完成角色分配的同步,因此请稍等片刻再继续检查。
如果loadAllSecrets()函数在完成执行之前就先调用了getFromCache()函数,那么说明启动顺序出现了问题。请打开server.js文件,确认await loadAllSecrets()语句确实位于app.listen()语句之前。如果顺序没有问题,那么可能只是因为密钥尚未被加载到Key Vault中。你可以运行az keyvault secret list --vault-name YOUR_VAULT命令来再次检查。
应用程序在本地可以正常启动,但在Azure App Service上却无法运行
几乎在这种情况下,问题都出在应用程序的配置设置上。要么是KEY_VAULT_NAME这个配置项根本不存在于App Service的配置文件中,要么就是 vault名称中存在拼写错误。请运行az webapp log tail命令来查看具体的启动错误信息,这样就能找出问题所在了。
AuthorizationFailed 这个错误通常会在执行az role assignment create命令时出现
您当前是Azure租户中的普通用户,没有分配角色的权限。因此,需要将现有的密钥库切换为访问策略模式——这样既无需重新创建密钥库,也不会导致您的机密信息丢失:
az keyvault update \
--name your-vault-name \
--resource-group keyvault-demo-rg \
--enable-rbac-authorization false
如果这种情况发生在“设置密钥库”阶段(即为自己分配访问权限时),请运行以下命令:
az keyvault set-policy \
--name your-vault-name \
--object-id $(az ad signed-in-user show --query id -o tsv) \
--secret-permissions get set list delete
如果这种情况发生在“为应用程序授予密钥库访问权限”阶段(即为应用程序分配管理身份访问权限时),请运行以下命令:
az keyvault set-policy \
--name your-vault-name \
--object-id $PRINCIPAL_ID \
--secret-permissions get list
如果密钥库返回“SecretNotFound”错误,说明该机密信息从未被添加到密钥库中,或者已经被删除,又或者其名称与您的代码中请求的名称不完全匹配——需要注意的是,密钥库中的机密名称是区分大小写的。“db-password”和“DB-PASSWORD”是两个不同的名称。请运行`az keyvault secret list --vault-name YOUR_VAULT`命令,查看密钥库中实际存在的机密信息,然后与`src/config/secrets.js`文件中`loadAllSecrets()`函数请求的名称进行对比。通常,问题出在大小写差异或多余的连字符上。
总结
这个项目中的`.env`文件只包含一个值:密钥库的名称。这个信息并不属于敏感数据。所有的机密信息——如数据库密码、API密钥、签名密钥等——都存储在密钥库中,而永远不会影响到您的代码或部署流程。
这就是我现在在Azure项目中采用的方案。我发现,启动检查功能在实际应用中非常有用:如果密钥库无法访问或某个机密信息缺失,服务器会立即退出并显示明确的错误信息,而不会在开始运行后很快就出现故障。这样,您就能立刻发现问题,而不会等到两小时后才收到难以理解的数据库连接错误提示。
如果要添加新的机密信息,只需将其放入密钥库中,然后将其名称添加到`secretNames`数组中即可。其余的所有配置都会自动随之调整。
完整的代码示例可以在GitHub上找到:nodejs-azure-keyvault