用户模拟
想象一下,TechCorp 的支持工程师 Sarah 收到了一张来自客户 Alex 的紧急工单,Alex 无法访问关键资源。为了高效地诊断和解决问题,Sarah 需要看到系统中 Alex 所看到的内容。这时,Logto 的用户模拟功能就派上用场了。
用户模拟允许像 Sarah 这样的授权用户在系统中临时代表其他用户(如 Alex)进行操作。这个强大的功能对于故障排除、提供客户支持和执行管理任务非常有价值。
它是如何工作的?
模拟过程涉及三个主要步骤:
- Sarah 通过 TechCorp 的后端服务器请求模拟
- TechCorp 的服务器从 Logto 的 Management API 获取主体令牌
- Sarah 的应用程序用这个主体令牌交换访问令牌
让我们看看 Sarah 如何使用这个功能来帮助 Alex。
步骤 1:请求模拟
首先,Sarah 的支持应用程序需要从 TechCorp 的后端服务器请求模拟。
请求(Sarah 的应用程序到 TechCorp 的服务器)
POST /api/request-impersonation HTTP/1.1
Host: api.techcorp.com
Authorization: Bearer <Sarah 的访问令牌>
Content-Type: application/json
{
"userId": "alex123",
"reason": "调查资源访问问题",
"ticketId": "TECH-1234"
}
在这个 API 中,后端应该执行适当的授权 (Authorization) 检查,以确保 Sarah 拥有模拟 Alex 的必要权限。
步骤 2:获取主体令牌
TechCorp 的服务器在验证 Sarah 的请求后,将调用 Logto 的 Management API 以获取主体令牌。
请求(TechCorp 的服务器到 Logto 的 Management API)
POST /api/subject-tokens HTTP/1.1
Host: techcorp.logto.app
Authorization: Bearer <TechCorp_m2m_access_token>
Content-Type: application/json
{
"userId": "alex123",
"context": {
"ticketId": "TECH-1234",
"reason": "资源访问问题",
"supportEngineerId": "sarah789"
}
}
响应(Logto 到 TechCorp 的服务器)
{
"subjectToken": "sub_7h32jf8sK3j2",
"expiresIn": 600
}
TechCorp 的服务器然后应该将这个主体令牌返回给 Sarah 的应用程序。
响应(TechCorp 的服务器到 Sarah 的应用程序)
{
"subjectToken": "sub_7h32jf8sK3j2",
"expiresIn": 600
}
步骤 3:用主体令牌交换访问令牌
现在,Sarah 的应用程序用这个主体令牌交换代表 Alex 的访问令牌,并指定令牌将被使用的资源。
请求(Sarah 的应用程序到 Logto 的令牌端点)
POST /oidc/token HTTP/1.1
Host: techcorp.logto.app
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&client_id=techcorp_support_app
&scope=resource:read
&subject_token=alx_7h32jf8sK3j2
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&resource=https://api.techcorp.com/customer-data
响应(Logto 到 Sarah 的应用程序)
{
"access_token": "eyJhbG...<truncated>",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "resource:read"
}
返回的 access_token
将绑定到指定的资源,确保它只能用于 TechCorp 的客户数据 API。
示例用法
以下是 Sarah 如何在 Node.js 支持应用程序中使用此功能:
interface ImpersonationResponse {
subjectToken: string;
expiresIn: number;
}
interface TokenExchangeResponse {
access_token: string;
issued_token_type: string;
token_type: string;
expires_in: number;
scope: string;
}
async function impersonateUser(
userId: string,
clientId: string,
ticketId: string,
resource: string
): Promise<string> {
try {
// 步骤 1 & 2:请求模拟并获取主体令牌
const impersonationResponse = await fetch(
'https://api.techcorp.com/api/request-impersonation',
{
method: 'POST',
headers: {
Authorization: "Bearer <Sarah 的访问令牌>",
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId,
reason: '调查资源访问问题',
ticketId,
}),
}
);
if (!impersonationResponse.ok) {
throw new Error(`HTTP 错误发生。状态:${impersonationResponse.status}`);
}
const { subjectToken } = (await impersonationResponse.json()) as ImpersonationResponse;
// 步骤 3:用主体令牌交换访问令牌
const tokenExchangeBody = new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
client_id: clientId,
scope: 'openid profile resource.read',
subject_token: subjectToken,
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
resource: resource,
});
const tokenExchangeResponse = await fetch('https://techcorp.logto.app/oidc/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: tokenExchangeBody,
});
if (!tokenExchangeResponse.ok) {
throw new Error(`HTTP 错误!状态:${tokenExchangeResponse.status}`);
}
const tokenData = (await tokenExchangeResponse.json()) as TokenExchangeResponse;
return tokenData.access_token;
} catch (error) {
console.error('模拟失败:', error);
throw error;
}
}
// Sarah 使用此函数模拟 Alex
async function performImpersonation(): Promise<void> {
try {
const accessToken = await impersonateUser(
'alex123',
'techcorp_support_app',
'TECH-1234',
'https://api.techcorp.com/customer-data'
);
console.log('Alex 的模拟访问令牌:', accessToken);
} catch (error) {
console.error('执行模拟失败:', error);
}
}
// 执行模拟
void performImpersonation()
- 主体令牌是短期的并且仅供一次使用。
- 模拟访问令牌不附带 刷新令牌。如果令牌在 Sarah 解决 Alex 的问题之前过期,Sarah 将需要重复此过程。
- TechCorp 的后端服务器必须实施适当的授权 (Authorization) 检查,以确保只有像 Sarah 这样的授权支持人员可以请求模拟。
act
声明
在使用令牌交换流程进行模拟时,签发的访问令牌可以包含一个额外的 act
(actor)声明。此声明表示“执行方”的身份——在我们的示例中,是执行模拟的 Sarah。
要包含 act
声明,Sarah 的应用程序需要在令牌交换请求中提供一个 actor_token
。此令牌应是一个有效的 Sarah 的访问令牌,并具有 openid
权限。以下是如何在令牌交换请求中包含它:
POST /oidc/token HTTP/1.1
Host: techcorp.logto.app
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&client_id=techcorp_support_app
&scope=resource:read
&subject_token=alx_7h32jf8sK3j2
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&actor_token=sarah_access_token
&actor_token_type=urn:ietf:params:oauth:token-type:access_token
&resource=https://api.techcorp.com/customer-data
如果提供了 actor_token
,生成的访问令牌将包含一个 act
声明,如下所示:
{
"aud": "https://api.techcorp.com",
"iss": "https://techcorp.logto.app",
"exp": 1443904177,
"sub": "alex123",
"act": {
"sub": "sarah789"
}
}
这个 act
声明清楚地表明 Sarah (sarah789) 正在代表 Alex (alex123) 行事。act
声明对于审计和跟踪模拟操作非常有用。
自定义令牌声明
Logto 允许你为模拟令牌 自定义令牌声明。这对于在模拟过程中添加额外的上下文或元数据非常有用,例如模拟的原因或相关的支持工单。
当 TechCorp 的服务器从 Logto 的 Management API 请求主体令牌时,它可以包含一个 context
对象:
{
"userId": "alex123",
"context": {
"ticketId": "TECH-1234",
"reason": "资源访问问题",
"supportEngineerId": "sarah789"
}
}
这个 context 然后可以在 getCustomJwtClaims()
函数中使用,以向最终访问令牌添加特定声明。以下是如何实现的示例:
const getCustomJwtClaims = async ({ token, context, environmentVariables }) => {
if (context.grant?.type === 'urn:ietf:params:oauth:grant-type:token-exchange') {
const { ticketId, reason, supportEngineerId } = context.grant.subjectTokenContext;
return {
impersonation_context: {
ticket_id: ticketId,
reason: reason,
support_engineer: supportEngineerId,
},
};
}
return {};
};
Sarah 收到的最终访问令牌可能如下所示:
{
"sub": "alex123",
"aud": "https://api.techcorp.com/customer-data",
"impersonation_context": {
"ticket_id": "TECH-1234",
"reason": "资源访问问题",
"support_engineer": "sarah789"
}
// ... 其他标准声明
}
通过这种方式自定义访问令牌声明,TechCorp 可以包含有关模拟上下文的有价值信息,使其更容易在系统中审计和理解模拟活动。
在向令牌添加自定义声明时要谨慎。避免包含敏感信息,如果令牌被截获或泄露,可能会带来安全风险。JWT 是签名的,但不是加密的,因此声明对任何有权访问令牌的人都是可见的。