你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn。
本文提供了在 Azure 通信服务 SDK 中管理用户访问令牌的最佳实践。 遵循本指南可帮助你优化应用程序使用的资源,并减少 Azure 通信标识 API 的往返次数。
通信令牌凭据
通信令牌凭据(凭据)指包装用户访问令牌的身份验证原语。 其用于对通信服务(例如聊天或呼叫)中的用户进行身份验证。 此外,该凭据还提供内置的令牌刷新功能,以方便开发人员。
选择会话生存期
根据方案,可能需要调整为应用程序颁发的令牌的有效期。 以下最佳做法或其组合可帮助你实现方案的最佳解决方案:
设置自定义令牌过期时间
请求新令牌时,我们建议对一次性聊天消息或具有时间限制的调用会话使用短生存期令牌,对使用应用程序时间较长的代理使用长生存期令牌。 默认令牌过期时间为 24 小时,但可以通过为可选参数提供一个介于 1 小时到 24 小时之间的值来自定义它,如下所示:
const tokenOptions = { tokenExpiresInMinutes: 60 };
const user = { communicationUserId: userId };
const scopes = ["chat"];
let communicationIdentityToken = await identityClient.getToken(user, scopes, tokenOptions);
静态令牌
对于生存期较短的客户端,使用静态令牌初始化凭据。 此方法适用于发送一次性聊天消息或限时调用会话等方案。
let communicationIdentityToken = await identityClient.getToken({ communicationUserId: userId }, ["chat", "voip"]);
const tokenCredential = new AzureCommunicationTokenCredential(communicationIdentityToken.token);
回调函数
对于生存期较长的客户端,使用回调函数初始化凭据,确保在通信过程中持续进行身份验证。 此方法适用于时间较长的调用会话。
const tokenCredential = new AzureCommunicationTokenCredential({
tokenRefresher: async (abortSignal) => fetchTokenFromMyServerForUser(abortSignal, "<user_name>")
});
令牌刷新
为正确实现令牌刷新程序回调,代码必须返回含有效 JSON Web 令牌的字符串。 无论何时,一定要返回有效令牌(将其到期日设置为未来日期)。 有些平台(如 JavaScript 和 .NET)会提供一种方法来中止刷新操作,并将AbortSignal 或 CancellationToken 传递给函数。 建议接受这些对象,并进一步利用或传递它们。
示例 1:刷新通信用户的令牌
假设我们在 Express 上构建了一个 Node.js 应用程序,且 /getToken 终结点允许获取按名称指定的用户的新有效令牌。
app.post('/getToken', async (req, res) => {
// Custom logic to determine the communication user id
let userId = await getCommunicationUserIdFromDb(req.body.username);
// Get a fresh token
const identityClient = new CommunicationIdentityClient("<COMMUNICATION_SERVICES_CONNECTION_STRING>");
let communicationIdentityToken = await identityClient.getToken({ communicationUserId: userId }, ["chat", "voip"]);
res.json({ communicationIdentityToken: communicationIdentityToken.token });
});
接下来,我们需要在客户端应用程序中实现令牌刷新程序回调,并正确利用 AbortSignal 返回已解包的 JWT 字符串。
const fetchTokenFromMyServerForUser = async function (abortSignal, username) {
const response = await fetch(`${HOST_URI}/getToken`,
{
method: "POST",
body: JSON.stringify({ username: username }),
signal: abortSignal,
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
const data = await response.json();
return data.communicationIdentityToken;
}
};
示例 2:刷新 Teams 用户的令牌
假设我们在 Express 上构建了一个 Node.js 应用程序,且 /getTokenForTeamsUser 终结点允许交换 Teams 用户的 Microsoft Entra 访问令牌,进而获取到期时间相匹配的新通信标识访问令牌。
app.post('/getTokenForTeamsUser', async (req, res) => {
const identityClient = new CommunicationIdentityClient("<COMMUNICATION_SERVICES_CONNECTION_STRING>");
let communicationIdentityToken = await identityClient.getTokenForTeamsUser(req.body.teamsToken, '<AAD_CLIENT_ID>', '<TEAMS_USER_OBJECT_ID>');
res.json({ communicationIdentityToken: communicationIdentityToken.token });
});
接下来,我们需要在客户端应用程序中实现令牌刷新程序回调,而其职责将为:
- 刷新 Teams 用户的 Microsoft Entra 访问令牌
- 将 Teams 用户的 Microsoft Entra 访问令牌交换为通信标识访问令牌
const fetchTokenFromMyServerForUser = async function (abortSignal, username) {
// 1. Refresh the Azure AD access token of the Teams User
let teamsTokenResponse = await refreshAadToken(abortSignal, username);
// 2. Exchange the Azure AD access token of the Teams User for a Communication Identity access token
const response = await fetch(`${HOST_URI}/getTokenForTeamsUser`,
{
method: "POST",
body: JSON.stringify({ teamsToken: teamsTokenResponse.accessToken }),
signal: abortSignal,
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
const data = await response.json();
return data.communicationIdentityToken;
}
}
在此示例中,我们使用 Microsoft 身份验证库 (MSAL) 来刷新 Microsoft Entra 访问令牌。 根据获取用于调用 API 的 Microsoft Entra 令牌这一指南,我们首先要尝试在未与用户交互的情况下获取令牌。 如果无法做到这一点,我们将触发一个交互流。
const refreshAadToken = async function (abortSignal, username) {
if (abortSignal.aborted === true) throw new Error("Operation canceled");
// MSAL.js v2 exposes several account APIs; the logic to determine which account to use is the responsibility of the developer.
// In this case, we'll use an account from the cache.
let account = (await publicClientApplication.getTokenCache().getAllAccounts()).find(u => u.username === username);
const renewRequest = {
scopes: [
"https://auth.msft.communication.azure.com/Teams.ManageCalls",
"https://auth.msft.communication.azure.com/Teams.ManageChats"
],
account: account,
forceRefresh: forceRefresh
};
let tokenResponse = null;
// Try to get the token silently without the user's interaction
await publicClientApplication.acquireTokenSilent(renewRequest).then(renewResponse => {
tokenResponse = renewResponse;
}).catch(async (error) => {
// In case of an InteractionRequired error, send the same request in an interactive call
if (error instanceof InteractionRequiredAuthError) {
// You can choose the popup or redirect experience (`acquireTokenPopup` or `acquireTokenRedirect` respectively)
publicClientApplication.acquireTokenPopup(renewRequest).then(function (renewInteractiveResponse) {
tokenResponse = renewInteractiveResponse;
}).catch(function (interactiveError) {
console.log(interactiveError);
});
}
});
return tokenResponse;
}
提供初始令牌
若要进一步优化代码,可以在应用程序启动时提取令牌并直接将其传递给凭据。 提供初始令牌将跳过对刷新程序回调函数的第一次调用,同时保留对该函数的所有后续调用。
const tokenCredential = new AzureCommunicationTokenCredential({
tokenRefresher: async () => fetchTokenFromMyServerForUser("<user_id>"),
token: "<initial_token>"
});
主动刷新令牌
使用主动刷新,消除在按需获取令牌的过程中可能出现的任何延迟。 主动刷新会在令牌生存期结束时于后台刷新令牌。 当令牌即将过期时,凭据会在其有效期结束前 10 分钟开始尝试检索令牌。 此过程将触发更高频率的刷新程序回调,直到成功检索到有效期足够长的令牌。
const tokenCredential = new AzureCommunicationTokenCredential({
tokenRefresher: async () => fetchTokenFromMyServerForUser("<user_id>"),
refreshProactively: true
});
如果要取消计划好的刷新任务,请释放凭据对象。
主动刷新 Teams 用户的令牌
若要最大程度减少 Azure 通信标识 API 的往返次数,请确保传递给 exchange 的 Microsoft Entra 令牌的有效期足够长(> 10 分钟)。 如果 MSAL 返回的缓存令牌的有效性较短,则可以使用以下选项来绕过缓存:
- 强制刷新令牌
- 将 MSAL 的令牌续订时段增加到超过 10 分钟
选项 1:触发令牌获取流,并将 AuthenticationParameters.forceRefresh 设置为 true。
// Extend the `refreshAadToken` function
const refreshAadToken = async function (abortSignal, username) {
// ... existing refresh logic
// Make sure the token has at least 10-minute lifetime and if not, force-renew it
if (tokenResponse.expiresOn < (Date.now() + (10 * 60 * 1000))) {
const renewRequest = {
scopes: [
"https://auth.msft.communication.azure.com/Teams.ManageCalls",
"https://auth.msft.communication.azure.com/Teams.ManageChats"
],
account: account,
forceRefresh: true // Force-refresh the token
};
await publicClientApplication.acquireTokenSilent(renewRequest).then(renewResponse => {
tokenResponse = renewResponse;
});
}
}
选项 2:通过使用自定义 SystemOptions.tokenRenewalOffsetSeconds 实例化 PublicClientApplication 来初始化 MSAL 身份验证上下文。
const publicClientApplication = new PublicClientApplication({
system: {
tokenRenewalOffsetSeconds: 900 // 15 minutes (by default 5 minutes)
});
取消刷新
为了使通信客户端能够取消正在进行的刷新任务,需要将取消对象传递给刷新程序回调。 请注意,此模式仅适用于 JavaScript 和 .NET。
var controller = new AbortController();
var joinChatBtn = document.querySelector('.joinChat');
var leaveChatBtn = document.querySelector('.leaveChat');
joinChatBtn.addEventListener('click', function () {
// Wrong:
const tokenCredentialWrong = new AzureCommunicationTokenCredential({
tokenRefresher: async () => fetchTokenFromMyServerForUser("<user_name>")
});
// Correct: Pass abortSignal through the arrow function
const tokenCredential = new AzureCommunicationTokenCredential({
tokenRefresher: async (abortSignal) => fetchTokenFromMyServerForUser(abortSignal, "<user_name>")
});
// ChatClient is now able to abort token refresh tasks
const chatClient = new ChatClient("<endpoint-url>", tokenCredential);
// Pass the abortSignal to the chat client through options
const createChatThreadResult = await chatClient.createChatThread(
{ topic: "Hello, World!" },
{
// ...
abortSignal: controller.signal
}
);
// ...
});
leaveChatBtn.addEventListener('click', function() {
controller.abort();
console.log('Leaving chat...');
});
如果要取消后续刷新任务,请释放凭据对象。
清理资源
由于凭据对象可以传递给多个聊天或呼叫客户端实例,因此 SDK 不会对其生存期做出任何假设,会将其处置责任留给开发人员。 由通信服务应用程序释放不再需要的凭据实例。 启用主动刷新时,释放凭据还会取消计划的刷新操作。
调用 .dispose() 函数。
const tokenCredential = new AzureCommunicationTokenCredential("<token>");
// Use the credential for Calling or Chat
const chatClient = new ChatClient("<endpoint-url>", tokenCredential);
// ...
tokenCredential.dispose()
处理注销
根据你的情况,你可能需要从一个或多个服务注销用户:
- 要从单个服务注销用户,请释放凭据对象。
- 要将用户从多个服务中注销,请实现一种信号机制来通知所有服务释放凭据对象,此外,还要撤销给定标识的所有访问令牌。
后续步骤
本文介绍了如何执行以下操作:
- 正确初始化和释放凭据对象
- 实现令牌刷新程序回调
- 优化令牌刷新逻辑
要了解详细信息,你可能想要浏览以下快速入门指南: