管理会话和权限
一旦玩家及其父母完成了同意流程(无论是通过 VPC 小部件 还是 自定义年龄门控),您的游戏会收到一个包含玩家权限的会话。但是,权限可能会随着时间的推移而改变:父母可能通过 Family Connect 调整设置,玩家可能过生日而进入新的年龄类别,或者有人可能请求访问其他功能。
本指南说明如何检测这些更改、适当响应,并向玩家清楚地传达,以便他们始终了解自己当前的功能访问权限。
当权限在游戏关闭时(甚至运行时)发生变化时,如果功能突然出现或消失,玩家可能会感到困惑。如果没有清晰的沟通,这些更改可能看起来像是错误,而不是来自父母的故意更新或与年龄相关的调整。
先决条件
在开始之前,请确保您具备:
- 已完成的同意流程:玩家应该已经通过完成 VPC 快速入门 或 自定义年龄门控快速入门 而拥有活动会话
- k-ID 产品:在 k-ID Compliance Studio 中创建并配置您的产品
- API 密钥:从 Compliance Studio 中您产品的开发者设置页面生成您的 API 密钥
- Webhook 端点(推荐):设置一个安全的 HTTPS 端点以接收会话事件。更多详情,请参阅 Webhooks。
理解会话和挑战
一旦玩家被其可信成人授予访问权限,他们每个产品恰好有一个会话。当权限发生变化时(无论是通过父母修改、年龄增长事件还是权限升级),相同的会话 ID 会用新权限更新。不会创建新会话;现有会话反映玩家访问的当前状态。但是,如果会话被撤销并且可信成人再次完成同意流程,则会创建一个具有新会话 ID 的新会话。
挑战是需要父母批准的同意请求。当挑战成功完成(PASS)时,会创建会话或使用授予的权限更新会话。相反,如果会话被删除,该玩家的所有未完成挑战都会自动设置为 FAIL。有关挑战的详细信息,请参阅 Challenges 概念指南。
权限如何变化
权限更改可能因多种原因发生:
| 更改类型 | 描述 | 检测方法 |
|---|---|---|
| 父母修改权限 | 父母使用 Family Connect 启用或禁用功能 | Webhook 或会话比较 |
| 玩家年龄增长 | 生日使玩家进入新的年龄类别。当玩家年龄增长到不再需要父母同意时,权限的 managedBy 设置为 PLAYER,允许玩家在没有父母同意的情况下直接控制它们 | 仅会话比较 |
| 会话删除 | 父母通过 Family Connect 撤销访问,导致会话被删除(返回 400) | Webhook 或会话比较(返回 400) |
了解这些场景有助于您实施正确的检测策略。
步骤 1:选择您的检测方法
您有两种检测权限更改的方法。大多数实现应该同时使用两者,将 webhook 作为主要方法,会话比较作为后备方法。
方法 A:基于 Webhook 的检测(推荐)
当您有可以接收 HTTP 回调的服务器时使用 webhook。
使用这种方法,当父母更改权限时,k-ID 会立即通知您的服务器。您的服务器更新自己的状态,游戏在下次启动或功能访问时读取更新后的状态。
优势:
- 实时通知,检测更改无延迟
- 比持续轮询资源使用更少
- 能够主动通知玩家
工作原理:
方法 B:重启时的会话比较
当您没有 webhook 时,或作为 webhook 的后备方法使用会话比较。
使用这种方法,您的游戏缓存最后已知的会话,并在每次重启时(或在游戏过程中定期)与来自 k-ID 的当前会话进行比较。
优势:
- 不需要 webhook 配置
- 捕获年龄增长更改(不会触发 webhook)
- 实现简单
工作原理:
步骤 2:实现基于 Webhook 的检测
如果您的游戏有服务器,请实现基于 webhook 的检测以进行实时权限更新。
所有 k-ID API 调用必须从您的服务器进行,而不是从客户端代码进行。您的 API 密钥永远不应暴露给游戏客户端。本指南中的示例显示服务器端代码(Node.js 和 Express)。您的游戏客户端应该与您自己的服务器通信,然后服务器调用 k-ID API。
配置您的 Webhook 端点
在 Compliance Studio 中,在您产品的开发者设置下配置您的 webhook URL。确保您的端点可以接收:
Session.ChangePermissions:当父母更改权限时触发Session.Delete:当会话被删除时触发
处理 Session.ChangePermissions Webhook
当 k-ID 触发此 webhook 时,更新您的服务器状态以标记会话已更改:
app.post("/webhook/k-id", (req, res) => {
const { eventType, data } = req.body;
switch (eventType) {
case "Session.ChangePermissions":
// Flag this session as having changed permissions
markSessionAsChanged(data.id, {
changeType: "permissions_updated",
changedAt: new Date().toISOString()
});
break;
case "Session.Delete":
// Parent revoked access - session has been deleted
// Note: All incomplete challenges for this player are automatically set to FAIL
markSessionAsDeleted(data.id);
break;
}
res.status(200).send("OK");
});
获取并比较权限
当您的游戏客户端连接到您的服务器时,检查更改标志,从 k-ID 获取更新的会话,并将其与您存储的版本进行比较以确定更改的内容:
// Server-side endpoint that your game client calls
app.get("/api/check-permissions/:sessionId", async (req, res) => {
const { sessionId } = req.params;
const changeInfo = await getChangeFlag(sessionId);
if (!changeInfo.hasChanged) {
return res.json({ hasChanged: false });
}
const cachedSession = await getStoredSession(sessionId);
const response = await fetch(
`https://game-api.k-id.com/api/v1/session/get?sessionId=${sessionId}`,
{ headers: { "Authorization": `Bearer ${process.env.KID_API_KEY}` } }
);
const { session: newSession } = await response.json();
const changes = comparePermissions(cachedSession, newSession);
await storeSession(sessionId, newSession);
await clearChangeFlag(sessionId);
res.json({
hasChanged: true,
changes: changes,
session: newSession
});
});
comparePermissions 函数(在步骤 3 中显示)准确识别哪些权限已更改,使您能够向玩家显示特定消息。
步骤 3:实现会话比较
实现会话比较作为后备方法(或者如果您没有 webhook,则作为主要方法)。这种方法对于检测不会触发 webhook 的年龄增长更改也是必不可少的。
Session.ChangePermissions webhook 通知您权限已更改,但不包括更改了什么。要确定哪些特定权限被启用或禁用,您必须获取更新的会话并将其与缓存版本进行比较。无论您使用 webhook 还是轮询,都需要此比较逻辑。
存储会话
每当您收到会话时,将其存储在您的服务器上(在数据库、缓存或与玩家关联的其他持久存储中):
// Server-side session storage
async function storeSession(sessionId, session) {
await db.sessions.upsert({
sessionId: sessionId,
session: session,
updatedAt: new Date().toISOString()
});
}
async function getStoredSession(sessionId) {
const record = await db.sessions.findOne({ sessionId });
return record?.session || null;
}
在游戏启动时比较会话
当您的游戏客户端启动时,它应该调用您的服务器以检查会话更改。您的服务器从 k-ID 获取当前会话并进行比较:
// Server-side endpoint that your game client calls on startup
app.get("/api/session/:sessionId", async (req, res) => {
const { sessionId } = req.params;
const cachedSession = await getStoredSession(sessionId);
if (!cachedSession) {
return res.json({ needsConsent: true });
}
const response = await fetch(
`https://game-api.k-id.com/api/v1/session/get?sessionId=${sessionId}&etag=${cachedSession.etag}`,
{ headers: { "Authorization": `Bearer ${process.env.KID_API_KEY}` } }
);
if (response.status === 304) {
return res.json({ hasChanged: false, session: cachedSession });
}
const { session: currentSession } = await response.json();
const changes = comparePermissions(cachedSession, currentSession);
await storeSession(sessionId, currentSession);
res.json({
hasChanged: changes.length > 0,
changes: changes,
session: currentSession
});
});
检测权限差异
比较旧会话和新会话以识别特定更改:
function comparePermissions(oldSession, newSession) {
const changes = [];
for (const newPerm of newSession.permissions) {
const oldPerm = oldSession.permissions.find(p => p.name === newPerm.name);
if (!oldPerm) {
changes.push({ type: "added", permission: newPerm.name, enabled: newPerm.enabled });
} else if (oldPerm.enabled !== newPerm.enabled) {
changes.push({
type: newPerm.enabled ? "enabled" : "disabled",
permission: newPerm.name,
previousState: oldPerm.enabled
});
} else if (oldPerm.managedBy !== newPerm.managedBy) {
changes.push({
type: "management_changed",
permission: newPerm.name,
previousManagedBy: oldPerm.managedBy,
newManagedBy: newPerm.managedBy
});
}
}
if (oldSession.ageStatus !== newSession.ageStatus) {
changes.push({
type: "age_status_changed",
previousStatus: oldSession.ageStatus,
newStatus: newSession.ageStatus
});
}
return changes;
}
当玩家年龄增长且不再需要父母同意时,k-ID 不会发送 webhook 通知。但是,当您比较会话时,您会注意到以前是 managedBy: "GUARDIAN" 的权限可能会更改为 managedBy: "PLAYER"。
当权限变为玩家管理时,玩家可以在没有父母同意的情况下直接控制它们。您应该更新 UI,允许玩家自己启用或禁用这些权限,而不是要求他们询问父母。当玩家通过 /session/upgrade API 请求启用 PLAYER 管理的权限时,它会自动启用而不会创建挑战。步骤 4 中的 updateFeatureAccess 函数显示了如何在 UI 中处理 PLAYER 管理的权限。
步骤 4:向玩家传达更改
当您检测到更改时,清楚地传达给玩家,以便他们了解功能更改的原因。这很重要:玩家永远不应该认为某些东西坏了。
有关传达权限更改、显示禁用功能和处理权限请求的详细设计建议,请参阅 UX 指南。
显示信息对话框
在客户端,显示一个对话框,说明更改了什么以及为什么(使用服务器返回的 changes 数组):
function showPermissionChangeDialog(changes) {
const disabledFeatures = changes
.filter(c => c.type === "disabled")
.map(c => getFeatureDisplayName(c.permission));
const enabledFeatures = changes
.filter(c => c.type === "enabled")
.map(c => getFeatureDisplayName(c.permission));
const ageChanged = changes.find(c => c.type === "age_status_changed");
let message = "";
if (ageChanged) {
// Player aged up
message = "Happy birthday! 🎉 Your permissions have been updated based on your new age.";
} else if (disabledFeatures.length > 0 && enabledFeatures.length === 0) {
// Parent restricted features
message = "Your parent has updated your permissions. " +
"The following features are no longer available:\n\n" +
disabledFeatures.map(f => `• ${f}`).join("\n");
} else if (enabledFeatures.length > 0 && disabledFeatures.length === 0) {
// Parent enabled features
message = "Great news! Your parent has enabled new features:\n\n" +
enabledFeatures.map(f => `• ${f}`).join("\n");
} else {
// Mixed changes
message = "Your permissions have been updated.";
if (enabledFeatures.length > 0) {
message += "\n\nNow available:\n" + enabledFeatures.map(f => `• ${f}`).join("\n");
}
if (disabledFeatures.length > 0) {
message += "\n\nNo longer available:\n" + disabledFeatures.map(f => `• ${f}`).join("\n");
}
}
showDialog({
title: "Permissions Updated",
message: message,
buttons: [{ text: "OK", action: "dismiss" }]
});
}
function getFeatureDisplayName(permissionName) {
const displayNames = {
"voice-chat": "Voice Chat",
"text-chat-private": "Private Messages",
"text-chat-public": "Public Chat",
"in-game-purchases": "In-Game Purchases",
"multiplayer": "Online Multiplayer",
// Add all your permissions here
};
return displayNames[permissionName] || permissionName;
}
优雅地处理禁用功能
在客户端,当功能被禁用时,确保 UI 清楚地反映这一点(使用服务器返回的会话数据):
function updateFeatureAccess(session) {
for (const permission of session.permissions) {
const featureElement = document.querySelector(`[data-feature="${permission.name}"]`);
if (!featureElement) continue;
if (!permission.enabled) {
featureElement.classList.add("feature-disabled");
if (permission.managedBy === "GUARDIAN") {
// Show "Ask parent" option
featureElement.setAttribute("data-disabled-reason", "parent");
featureElement.querySelector(".disabled-message").textContent =
"Ask a parent to enable this feature";
} else if (permission.managedBy === "PROHIBITED") {
// Feature is not available in this jurisdiction
featureElement.setAttribute("data-disabled-reason", "prohibited");
featureElement.querySelector(".disabled-message").textContent =
"This feature is not available";
} else if (permission.managedBy === "PLAYER") {
featureElement.setAttribute("data-disabled-reason", "player-choice");
featureElement.querySelector(".disabled-message").textContent = "Tap to enable";
featureElement.addEventListener("click", () => togglePlayerPermission(permission.name));
}
} else {
featureElement.classList.remove("feature-disabled");
if (permission.managedBy === "PLAYER") {
featureElement.setAttribute("data-managed-by", "player");
}
}
}
}
当 managedBy 是 PLAYER 时,玩家可以在没有父母同意的情况下直接控制权限。这通常发生在玩家年龄增长之后。您应该提供 UI 控件,如切换和按钮,允许玩家启用或禁用这些权限。当玩家通过 /session/upgrade API 请求启用 PLAYER 管理的权限时,它会自动启用而不会创建挑战(不需要父母同意)。
步骤 5:处理会话删除
当父母通过 Family Connect 撤销玩家对您产品的所有访问权限时,会话会在最后一步被删除。会话只是消失:查询返回 HTTP 400。当这种情况发生时(通过 webhook 或在会话比较期间检测到),玩家必须再次完成年龄门控和同意流程才能重新获得访问权限。
理解会话删除
当父母撤销对您的游戏或产品的访问权限时:
- 会话被删除 - 作为最后一步,会话被移除。查询返回 HTTP 400,使其看起来好像会话从未存在过
- 所有未完成的挑战都失败 - 该玩家的所有待处理挑战都会自动设置为
FAIL - 发送 Webhook 事件 - 您将收到有关删除的 webhook 通知
k-ID API 仅返回 ACTIVE 或 HOLD 作为有效会话状态。当父母撤销访问权限时,会话被删除,API 返回 400(未找到)。这是有意的:一旦会话被删除,应该将其视为不再存在。已删除的会话实际上“未找到”,因为玩家不再有权访问您的游戏。
检测已删除的会话
通过 Webhook(服务器端):
case "Session.Delete":
await deleteStoredSession(data.id);
await markSessionAsDeleted(data.id);
break;
在获取会话时通过 API(服务器端):
const response = await fetch(
`https://game-api.k-id.com/api/v1/session/get?sessionId=${sessionId}`,
{ headers: { "Authorization": `Bearer ${process.env.KID_API_KEY}` } }
);
if (response.status === 400) {
const error = await response.json();
if (error.error === "NOT_FOUND") {
await deleteStoredSession(sessionId);
return res.json({ sessionDeleted: true });
}
}
查询会话时带有 NOT_FOUND 错误的 400 响应可能意味着:
- 会话从未创建
- 会话已删除
- 会话 ID 无效
您的应用程序应该以相同的方式处理所有这些情况:无论根本原因如何,都将其视为表示玩家没有访问权限。不要仅依赖轮询,而是使用 webhook 接收有关会话删除的实时通知。
重定向到年龄门控
当您的游戏客户端从服务器收到 sessionDeleted: true 响应时,重新启动年龄门控流程:
// Client-side handling
async function checkSession(sessionId) {
const response = await fetch(`/api/session/${sessionId}`);
const data = await response.json();
if (data.sessionDeleted || data.needsConsent) {
// Show explanation to the player
showDialog({
title: "Session Ended",
message: "Your session has ended. Please complete age verification to continue playing.",
buttons: [{
text: "Continue",
action: () => navigateToAgeGate()
}]
});
}
}
步骤 6:为新权限提供升级路径
玩家可能想要请求访问需要父母同意的功能。/session/upgrade API 使此流程成为可能。
检查是否可以升级
在显示“询问父母”按钮之前,检查是否可以升级权限。此逻辑可以使用服务器返回的会话数据在客户端运行:
// Client-side check using session data from your server
function canRequestPermission(session, permissionName) {
const permission = session.permissions.find(p => p.name === permissionName);
if (!permission) return false;
// Can only request if currently disabled and managed by GUARDIAN
return !permission.enabled && permission.managedBy === "GUARDIAN";
}
请求权限升级
当玩家点击“询问父母”时,您的游戏客户端调用您的服务器,然后服务器调用 k-ID 会话升级 API:
// Server-side endpoint
app.post("/api/request-permission", async (req, res) => {
const { sessionId, permissionName } = req.body;
const response = await fetch(
"https://game-api.k-id.com/api/v1/session/upgrade",
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KID_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
sessionId: sessionId,
requestedPermissions: [{ name: permissionName }]
})
}
);
const result = await response.json();
if (result.status === "PASS") {
// Permission was enabled immediately (player can manage it)
// Update stored session
await storeSession(sessionId, result.session);
return res.json({ success: true, session: result.session });
} else if (result.status === "CHALLENGE") {
// Parent consent required
return res.json({
success: false,
requiresConsent: true,
challenge: result.challenge
});
}
});
选项 A:使用会话升级小部件(推荐)
会话升级小部件提供了一个完整的、预构建的界面,供父母审查和批准权限请求。如果您正在构建基于 Web 的游戏或可以显示 iframe,这是最简单的方法。
会话升级小部件仅适用于监护人管理的会话。如果玩家的会话是玩家管理的(其中 managedBy 是 "PLAYER"),则可以通过 /session/upgrade API 直接启用权限而不会创建挑战,因此不需要小部件。
当返回挑战时,生成小部件 URL 并向父母显示:
// Server-side endpoint to generate the widget URL
app.post("/api/permission-widget", async (req, res) => {
const { challengeId, parentEmail } = req.body;
const response = await fetch(
"https://game-api.k-id.com/api/v1/widget/generate-session-upgrade-url",
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KID_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
challengeId: challengeId,
email: parentEmail
})
}
);
const { url } = await response.json();
res.json({ widgetUrl: url });
});
在客户端,在 iframe 中显示小部件:
<iframe
id="permission-widget"
src="WIDGET_URL"
width="100%"
height="600"
allow="camera;payment;publickey-credentials-get;publickey-credentials-create"
frameborder="0">
</iframe>
小部件处理整个同意流程,包括父母验证和权限批准。监听 Widget.ExitReview DOM 事件以了解流程何时完成,然后从服务器刷新会话。
选项 B:构建自定义同意流程
如果您需要完全控制 UI 或无法使用 iframe,请构建自定义同意流程。为父母提供同意选项:
// Client-side UI
function showConsentRequest(challenge) {
showDialog({
title: "Ask a Parent",
message: "A parent needs to approve this feature. How would you like to reach them?",
options: [
{
label: "Send an email",
action: () => showEmailInput(challenge.challengeId)
},
{
label: "Show QR code",
action: () => showQRCode(challenge.url)
},
{
label: "Show code",
sublabel: `Go to asktoplay.com and enter: ${challenge.oneTimePassword}`,
action: () => showCodeDisplay(challenge.oneTimePassword)
}
]
});
}
有关处理自定义同意流程的更多详细信息,请参阅 自定义年龄门控快速入门。
最佳实践
性能建议
- 尽可能优先使用 webhook:因为它们更高效并提供实时更新
- 使用
etag参数:在调用/session/get时避免不必要的数据传输 - 在本地缓存会话:仅在需要时获取更新
- 不要频繁轮询:如果必须轮询,在游戏过程中至少等待 30 秒,或仅在游戏启动时检查
处理边缘情况
- 离线玩家:缓存会话并应用缓存的权限。当连接恢复时检查更新。
- 多个设备:如果玩家可以使用多个设备,请在与他们的帐户关联的云存储中存储会话,以确保一致性。
- 游戏过程中的年龄增长:对于长时间的游戏会话,考虑定期刷新会话以捕获生日触发的更改。
下一步
现在您已经实现了会话和权限管理,请探索这些资源:
- Sessions:深入了解会话生命周期和结构
- Challenges:同意挑战、状态处理和最佳实践的完整指南
- Permissions:有关权限类型和管理的详细信息
- Permissions:有关升级流程的更多详细信息
- Webhooks:Webhook 实现和验证的完整指南
Session.ChangePermissions:Webhook 事件参考- Best practices:其他实施指导