
本文详解如何在启用了 App Service 身份验证(AAD)的 Azure Linux 函数中,安全提取并复用前端(如静态 Web 应用)传递的 Bearer Token,构造 AuthorizationCodeCredential 或 ClientSecretCredential,从而以当前用户身份访问 Blob 存储等 Azure 资源,绕过 DefaultAzureCredential 在无托管身份环境下的失效问题。
本文详解如何在启用了 app service 身份验证(aad)的 azure linux 函数中,安全提取并复用前端(如静态 web 应用)传递的 bearer token,构造 `authorizationcodecredential` 或 `clientsecretcredential`,从而以当前用户身份访问 blob 存储等 azure 资源,绕过 `defaultazurecredential` 在无托管身份环境下的失效问题。
在 Azure Functions 中启用 App Service 身份验证(AAD)后,请求头中的 Authorization: Bearer <token> 会被自动解析,并通过 req.user 暴露用户声明(claims),但该 token 不会被 DefaultAzureCredential 自动识别或复用——因为 DefaultAzureCredential 仅尝试从环境变量、托管身份、Azure CLI 等后端可信源获取凭据,而不解析 HTTP 请求上下文中的用户令牌。因此,直接使用 new DefaultAzureCredential() 会抛出 CredentialUnavailableError: EnvironmentCredential is unavailable 错误。
要实现“以调用用户身份访问 Azure 资源”,核心思路是:将 req.headers.authorization 中的 Bearer Token 提取出来,结合 Azure AD 应用配置,构造一个能代表该用户的 TokenCredential 实例。推荐方案如下:
✅ 推荐方案:使用 ClientSecretCredential + 用户 Token(适用于服务到服务委托)
若你的函数应用已注册为 Azure AD 应用(建议启用 User.Read、Storage Blob Data Contributor 等委派权限),且你拥有该应用的 client_id、tenant_id 和 client_secret,可利用用户提供的 token 作为 authority 的上下文,配合 ClientSecretCredential 实现基于用户上下文的令牌交换(On-Behalf-Of 流)。但注意:JS SDK 当前(v3.x)原生不支持 OBO 流;更稳妥、广泛支持的方式是:
✅ 实用方案:提取并复用原始 Bearer Token(零额外鉴权开销)
由于 req.user 已证明认证成功,且你持有有效的 AAD 访问令牌(即 req.headers.authorization.split(' ')[1]),可直接将其封装为自定义 TokenCredential,供 Azure SDK 使用:
import { TokenCredential, AccessToken } from "@azure/identity";
import { BlobServiceClient } from "@azure/storage-blob";
// 自定义凭证:直接返回传入的用户访问令牌
class UserTokenCredential implements TokenCredential {
private readonly token: string;
constructor(token: string) {
this.token = token;
}
async getToken(_: string[]): Promise<AccessToken | null> {
return {
token: this.token,
expiresOnTimestamp: Date.now() + 3600 * 1000 // 安全起见,按典型 1h 过期设置(实际应解析 JWT exp 字段)
};
}
}
// 在函数主逻辑中使用
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
const authHeader = req.headers?.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
context.res = { status: 401, body: "Unauthorized: Missing Bearer token" };
return;
}
const userToken = authHeader.split(" ")[1];
try {
const credential = new UserTokenCredential(userToken);
const blobServiceClient = new BlobServiceClient(
`https://${accountName}.blob.core.windows.net`,
credential
);
const containerClient = blobServiceClient.getContainerClient(containerName);
const blobClient = containerClient.getBlobClient(blobName);
const downloadResponse = await blobClient.download();
// 处理流数据...
const downloadedContent = await streamToBuffer(downloadResponse.readableStreamBody);
context.res = { status: 200, body: { content: downloadedContent.toString() } };
} catch (err) {
context.log.error("Blob access failed:", err);
context.res = { status: 500, body: "Failed to access storage" };
}
};
// 辅助函数:流转 Buffer
async function streamToBuffer(readableStream: NodeJS.ReadableStream | undefined): Promise<Buffer> {
if (!readableStream) throw new Error("ReadableStream is null");
return new Promise((resolve, reject) => {
const chunks: Uint8Array[] = [];
readableStream.on("data", (data) => chunks.push(data));
readableStream.on("end", () => resolve(Buffer.concat(chunks)));
readableStream.on("error", reject);
});
}
export default httpTrigger;⚠️ 关键注意事项
- 权限配置必须匹配:确保用于签发用户 token 的 Azure AD 应用(即你的静态 Web 应用所用的注册应用)已在目标存储账户上授予 Storage Blob Data Reader/Contributor 等 RBAC 角色,或在 Storage 账户的 Access Control (IAM) 中为该应用的服务主体(而非用户)分配角色(推荐后者,更可控)。
- Token 有效性校验:生产环境应解析 JWT(如使用 jsonwebtoken 库)验证 aud(受众是否为你的函数或目标资源)、iss(签发者)、exp(过期时间),避免凭据滥用。
- 不要硬编码密钥:client_secret 等敏感信息务必存于 Function App 的 Application Settings(环境变量)中,通过 process.env.CLIENT_SECRET 读取。
- 替代方案说明:若需更高安全性与标准协议支持,可考虑在函数中部署 Microsoft.Identity.Web 的轻量版中间件(Node.js 生态支持有限),或改用 Azure API Management 作为认证网关统一处理令牌委派。
✅ 总结
DefaultAzureCredential 不适用于解析 HTTP 请求中的用户令牌场景。正确路径是:显式提取 Authorization 头中的 Bearer Token → 封装为 TokenCredential → 传入 Azure SDK 客户端。该方法零依赖外部凭据源,完全复用已有认证链,既简洁又符合最小权限原则。只需确保后端资源(如 Blob 存储)已为对应 AAD 应用或用户组配置了正确的 RBAC 权限,即可实现安全、可审计的用户级资源访问。










