mcpField
I
internet-identity

internet-identity

github.com/dfinity安全39337 次安装今天更新
代码仓库

关于 internet-identity

为 Internet Computer 应用实现 Internet Identity 身份验证的参考技能,面向使用 IC SDK 的开发者,重点说明登录、会话、SSO 和身份属性的正确处理。

主要功能

  1. 01通行密钥与 OpenID 登录指南
  2. 02组织 SSO 配置指南
  3. 03会话与子域名共享模式
  4. 04身份属性验证指南
  5. 05本地 Internet Identity 开发设置

适用场景

  1. 01为 IC 应用添加 Internet Identity 登录
  2. 02配置同级子域名间的登录共享
  3. 03在后端读取已验证的身份属性

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dfinity/icskills --skill internet-identity

安装后在 Claude Code 中输入/internet-identity确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
app-metadata.md
enterprise-sso.md
older-api.md
rust-identity-attributes.md
shared-sessions.md

详细介绍

Internet Identity 身份验证

概述

Internet Identity(II)是 Internet Computer 的原生身份验证系统。用户可以使用通行密钥、OpenID 账户(Google、Apple、Microsoft),或通过所属组织自己的 SSO 登录 II 驱动的应用;无需用户名或密码。每位用户在每个应用源下都有唯一的 principal,从而避免跨应用跟踪。

本技能的参考资料位于 references/:

  • references/shared-sessions.md:如何在同级子域名之间共享一次登录,涵盖端到端流程。
  • references/app-metadata.md:/.well-known/ii-app-metadata 的所有验证规则。
  • references/enterprise-sso.md:SSO 的组织端(OIDC 客户端、发现文件、限制、缓存)。
  • references/rust-identity-attributes.md:用于身份属性的 Rust 后端,以及 identity-attributes crate。
  • references/older-api.md:@icp-sdk/auth 10.x 及更早版本的差异。

前置条件

  • @icp-sdk/auth@^11 和 @icp-sdk/core@^6。两个主版本必须保持配套。 auth 11 和 10 的 peer 依赖均为 @icp-sdk/core@^6;auth 9 的 peer 依赖为 ^5。
  • 对于 Motoko 后端:需要 mo:identity-attributes >= 0.4.0(mops),这是注入两个登录方法并验证属性包的 mixin。它会引入 mo:core >= 2.5.0,并要求 moc >= 1.6.0 才能使用 include。

Canister ID

CanisterIDURL用途
Internet Identity(后端)rdmx6-jaaaa-aaaaa-aaadq-cai签发委托,签署属性包
Internet Identity(前端)uqzsh-gqaaa-aaaaq-qaada-caihttps://id.ai提供登录界面;authorizeUrl 指向此处

这两个 ID 在主网和本地网络上相同。请将其硬编码。

会导致构建失败的错误

  1. II 环境 URL 错误。 authorizeUrl 指向前端 canister:主网上为 https://id.ai/authorize,本地 II(ii: true)则为 http://id.ai.localhost:8000/authorize。系统会原样使用此 URL,因此必须包含 /authorize:https://id.ai 会打开 II 主页,且永远不会返回委托。

  2. 将 identityProvider 设为字符串,或只提供其中一部分。 它是一个对象 { authorizeUrl, canisterId },两项都必须提供;传入字符串或 URL 会抛出 TypeError。完全省略此选项即可使用主网 II,这是大多数应用需要的设置。

  3. 在前端代码中使用顶层 await。 Vite 6 及更早版本默认面向 es2020 时代的浏览器,这些浏览器不支持顶层 await,因此构建会失败;Vite 7 则允许使用。请将初始化代码包装在 async function init() 中(或放入框架生命周期钩子)。这种写法在所有版本中都能构建。不要为了让编译通过而将 build.target 改为 esnext。

  4. 把 maxTimeToLive 当作签名密钥的有效期。 它限制的是 II 中的会话;maxTimeToIdle 则会结束无人使用的会话。调用所用的委托有效期较短,并由客户端替换。除非应用有自己的策略,否则不要设置这两个选项:II 默认采用空闲七天、总计三十天的期限。

  5. 未等待 signIn(),或省略 try/catch。 用户关闭弹出窗口或身份验证失败时,signIn() 会被拒绝。

  6. 在浏览器代码中使用 shouldFetchRootKey 或 fetchRootKey()。 ic_env cookie(由前端 canister 或 Vite 开发服务器设置)会通过 IC_ROOT_KEY 携带根密钥:将其作为 rootKey 传给 HttpAgent.create(),并保持 host 未设置。运行时获取根密钥会让中间人能够在主网上替换成伪造的根密钥。

  7. 得到 2vxsx-fae 作为 principal。 这是匿名 principal:身份验证已静默失败。常见原因包括:authorizeUrl 错误(尤其是缺少 /authorize)、未处理 signIn() 的拒绝,或在 signIn() 完成前读取身份。如果存在登录流程,但当前源没有对应凭据,getIdentity() 会抛出 SessionNotHeldError(而不是返回匿名身份)。

  8. 将主体作为参数传递。 后端从 IC 协议读取调用方:Motoko 中为 msg.caller,Rust 中为 ic_cdk::api::msg_caller()。有关访问控制模式,请参阅 canister-security 技能。

  9. 为网关域名添加 derivationOrigin 或 ii-alternative-origins。 II 会将 ic0.app、icp0.io 和 icp.net 规范化为同一种形式,因此托管在其中任一域名上的 canister 都会获得相同的 principal。添加此配置会破坏身份验证。此处出现不同的 principal,意味着使用了不同的通行密钥或设备。确实需要添加它的情况是使用第二个源(自定义域名):请参阅“在多个源上托管应用”。

  10. 在前端生成属性 nonce。 传给 requestAttributes 的 nonce 必须来自后端(_internet_identity_sign_in_start),否则 canister 无法检查 bundle 的 implicit:nonce 是否由其签发,重放保护也就失效了。

  11. 读取属性数据时不验证签名者。 IC 会验证签名,但不会验证签名者身份:任何 canister 都能生成有效的 bundle。受信任的签名者是 rdmx6-jaaaa-aaaaa-aaadq-cai。使用 identity-attributes 库(Motoko 中的 mo:identity-attributes mixin,Rust 中的 #[identity_attributes] 属性),并配置 trusted_attribute_signers 和 frontend_origins。手写的 Rust 代码必须在读取 msg_caller_info_data() 前检查 msg_caller_info_signer(),否则攻击者 canister 可以伪造 email = "[email protected]"。

  12. 替换 Microsoft scoped-key 前缀中的 {tid}。 microsoft 提供方 URL 是字面值 https://login.microsoftonline.com/{tid}/v2.0;密钥形式必须准确为 openid:https://login.microsoftonline.com/{tid}/v2.0:email。填入租户 ID 会导致所有查询都无法匹配。

  13. 将 email 视为已验证。 email 是用户 II 关联账户中的原始地址;II 不会检查它,因此应将其视为用户输入。只有在 II 确认用户控制该地址时才会提供 verified_email:即 OpenID 提供方(例如 Google)将其标记为已验证,或用户直接在 II 中关联并验证了该邮箱。只能依据 verified_email 授予访问权限。sso: 下没有 verified_email:组织的 SSO 无法提供该属性;其 sso:<domain>:email 由该组织自己的提供方声明,因此只信任你列出的域名。

  14. 共享 Cookie 域名,却没有共享 derivationOrigin。 只有共享同一个 principal 的子域名才能共享登录状态。如果没有一个经授权、供所有子域名使用的派生源,共享记录中记录的账户就不可能属于读取源,/reauth 会让用户陷入无限跳转。请参阅 references/shared-sessions.md。

  15. 静默重新签发时不提供 hint、使用默认传输方式,或回调未声明。 当 II 中有多个会话时,不提供 hint 的 prompt: 'none' 会以 InteractionRequiredError(reason 为 account_selection_required)失败。此操作会在页面加载时运行,不需要用户手势,因此必须在独立路由上使用 transport: 'redirect',并在该源的 /.well-known/ii-auth-callbacks 中声明;否则重定向不会返回。

  16. 在错误的源上提供 /.well-known/ii-app-metadata、未启用 CORS,或有字段无效。 II 会从派生源(如已设置)或请求源读取此文件;文档和 Logo 都需要启用 CORS,而且任一字段无效都会导致整个文档无效。请参阅 references/app-metadata.md。

  17. 安装不兼容的主版本。 auth 11 要求对等依赖 @icp-sdk/core@^6;如果习惯性地将 core 固定为 ^5,就会出现:

    npm error ERESOLVE unable to resolve dependency tree
    npm error Found: @icp-sdk/[email protected]
    npm error peer @icp-sdk/core@"^6" from @icp-sdk/[email protected]
    

    不要用 --legacy-peer-deps 或 --force 来消除错误。将 @icp-sdk/auth@^11 与 @icp-sdk/core@^6 固定搭配;如果有其他依赖让你必须使用 core 5,则继续使用 @icp-sdk/auth@^9。

  18. 未设置 agentOptions 时登录本地 II。 客户端通过代理调用 II canister 来创建委托,该代理默认使用主网根密钥进行验证。使用本地 II 时,身份验证流程会完成,但创建委托随后会失败,并报错 TrustError: Certificate verification error("Invalid signature")。从 ic_env cookie 传入 agentOptions: { rootKey },并将 host 留空。较旧网络启动器启动的本地 II 缺少创建委托所需的方法:运行 icp network update 并重启网络。

  19. 根据委托的过期时间安排登出。 委托 getIdentity() 签名使用的身份有效期很短,会随着时间推移而更换,因此根据 identity.getDelegation() 的过期时间设置计时器,会在几分钟后将用户登出。会话结束时会收到 expired 状态:订阅该状态,并在 isAuthenticated() 变为 false 时退出已登录视图。

  20. 在浏览器中检查 SSO 域名。 不要 fetch('https://<domain>/.well-known/ii-openid-configuration'),也不要查找 isValidSsoDomain(已在 11.x 中移除)。使用 ssoDomain 创建客户端,并读取 getSsoStatus():II 会按照与登录时相同的规则检查域名,组织无需设置 CORS 标头。

  21. 在 signIn() 前等待 SSO 检查,或用 try/catch 包裹构造函数来处理域名。 必须在点击处理程序中同步调用 signIn(),否则 Safari 会阻止弹出窗口。无论 SSO 状态如何,包括 invalid,登录都会继续(II 自带的界面会显示进度和错误)。在任何状态下,如果用户关闭弹出窗口或身份验证失败,登录都会被拒绝,因此请添加拒绝处理程序。域名格式错误时,构造函数不会抛出异常,而是报告 invalid。

  22. 更改现有客户端的 ssoDomain 或 openIdProvider。 与其他所有选项一样,这两个选项只能在构造时设置;没有 setter,也不能针对每次 signIn() 单独覆盖。要使用新域名,请创建一个新客户端,并对被替换的客户端调用 dispose()。

在本地开发期间使用 II

默认方式:从本地网络使用主网 II。 使用 icp-cli >= 0.2.4 时,本地网络会信任主网子网的签名,因此来自 https://id.ai 的委托可用于访问本地部署的后端。创建客户端时不传入 identityProvider。

**备用方式:使用本地 II,**仅用于完全离线开发或指定的 II 构建版本。在 icp.yaml 的本地网络配置中添加 ii: true:

networks:
  - name: local
    mode: managed
    ii: true

随后 II 会在 http://id.ai.localhost:8000 提供服务,canister ID 保持不变。项目中无需添加 canister 条目。传入本地根密钥:

const authClient = new AuthClient({
  identityProvider: {
    authorizeUrl: "http://id.ai.localhost:8000/authorize",
    canisterId: "rdmx6-jaaaa-aaaaa-aaadq-cai",
  },
  agentOptions: { rootKey: canisterEnv?.IC_ROOT_KEY },
});

前端:登录流程

与框架无关;请按需适配 DOM 部分。

import { AuthClient } from "@icp-sdk/auth/client";
import { HttpAgent, Actor } from "@icp-sdk/core/agent";
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";

// The ic_env cookie carries the root key and canister IDs, locally and on mainnet.
const canisterEnv = safeGetCanisterEnv();

// Mainnet II is the default. derivationOrigin, openIdProvider, ssoDomain,
// stateStorage, transport, prompt, and hint are also constructor options.
const authClient = new AuthClient();

async function signIn() {
  try {
    // Optional session bounds: signIn({ maxTimeToIdle, maxTimeToLive }), in ns.
    return await authClient.signIn();
  } catch (error) {
    console.error("Sign-in failed:", error);
    throw error;
  }
}

// Ends the session at II: every tab of this origin is signed out.
async function signOut() {
  await authClient.signOut();
}

// No host: the default resolves correctly locally, on mainnet, and on custom domains.
async function createAuthenticatedActor(identity, canisterId, idlFactory) {
  const agent = await HttpAgent.create({ identity, rootKey: canisterEnv?.IC_ROOT_KEY });
  return Actor.createActor(idlFactory, { agent, canisterId });
}

// Wrapped in a function: Vite 6 and earlier reject top-level await by default.
async function init() {
  // isAuthenticated() is sync; getIdentity() is async.
  if (authClient.isAuthenticated()) {
    const identity = await authClient.getIdentity();
    const actor = await createAuthenticatedActor(identity, canisterId, idlFactory);
  }

  // getStatus(): 'signed-in' | 'signed-in-elsewhere' | 'expired' | 'signed-out'.
  // subscribe() fires on any change, including in another tab.
  authClient.subscribe(() => render(authClient.getStatus()));
}

init();

生命周期。 每个页面使用一个客户端,或每个组件使用一个客户端:两者都会读取和写入同一登录状态。视图所拥有的客户端会随视图一同释放:dispose() 会释放其监听器、订阅和计划的刷新操作,但不会登出。

一键登录

使用 openIdProvider('google'、'apple'、'microsoft')或 ssoDomain(组织的域名)可跳过 II 的方式选择界面。两者互斥(类型错误,且构造时会抛出异常),并且只能在构造时设置。

const google = new AuthClient({ openIdProvider: "google" });
const acme = new AuthClient({ ssoDomain: "acme.com" });
检查组织的域名

使用 ssoDomain 构建的客户端会在创建后立即通过 II 检查自己的域名,并像会话一样报告结果:通过 getSsoStatus() 获取同步快照,通过 subscribe() 接收变更通知。不会抛出异常,也不需要 AbortSignal。

type SsoStatus =
  | { state: "checking" }
  | { state: "available"; name?: string }      // name from the org's file: "Continue with Acme Corp"
  | { state: "invalid"; reason: string }       // not a domain: show reason, the user fixes the input
  | { state: "unavailable"; retryAfter?: Date; reason?: string }; // no SSO there, or failing; reason is II's, when given

在用户输入时进行检查:

let client;
let timer;

input.addEventListener("input", () => {
  client?.dispose();  // also stops the old client's check
  client = undefined; // Continue never signs in to the old domain
  showSpinner();

  clearTimeout(timer); // debounce: one client per pause, not per keystroke
  timer = setTimeout(() => {
    const next = new AuthClient({ ssoDomain: input.value }); // never throws for the domain
    next.subscribe(() => render(next.getSsoStatus()));
    render(next.getSsoStatus());
    client = next;
  }, 300);
});

function render(sso) {
  switch (sso.state) {
    case "checking":    return showSpinner();
    case "available":   return enableContinue(sso.name);
    case "invalid":     return showNotADomain(sso.reason);
    case "unavailable": return showUnavailable(sso.retryAfter, sso.reason);
  }
}

// Synchronous in the click handler: never await the check first.
continueButton.addEventListener("click", () => {
  client?.signIn().then(showApp, showSignInError);
});
  • 被取代的客户端会在发布结果前被释放,因此不会渲染过期结果。像上文那样对输入进行防抖:每输入一个字符就构建一个客户端会导致客户端和检查频繁更替。
  • refreshSsoStatus() 会重新运行检查,可用于“重试”按钮。当 retryAfter 尚未到期时,II 会立即再次返回 unavailable,不会发起新的请求,因此应禁用按钮并显示倒计时(“2 分钟后重试”)。对 invalid 客户端调用此方法不会产生任何效果:用户需要修正输入,这会构建一个新客户端。客户端不会自行重新检查。
  • 在所有状态下都可以调用 signIn():II 会自行解析域名,并显示自己的加载和错误界面,即使值为 invalid 也一样。任何状态下,弹出窗口关闭或身份验证失败都会导致调用被拒绝。
  • invalid 遵循 II 自身的规则:至少包含两个标签,每个标签由 1 至 63 个字母、数字或连字符组成,连字符不能位于标签开头或结尾,标签的第三和第四个位置不能同时为 --(xn-- punycode 除外),总长度最多为 253 个字符,且只有回环主机(localhost、127.0.0.1 或 .localhost 下的名称)可以带端口。其 reason 会列出发现的所有问题。
  • 此检查会预热 II 的缓存,因此在同一客户端上调用 signIn() 时会更快启动。

有关组织如何设置其一侧的说明(OIDC 客户端、ii-openid-configuration 文件、逐应用访问权限、拒绝该文件的限制、缓存):references/enterprise-sso.md。

身份属性

当后端需要主体之外的信息(例如经过验证的电子邮件地址)时,II 会在委托凭证旁返回一个签名属性包。后端提供两个方法:_internet_identity_sign_in_start 用于生成 nonce,_internet_identity_sign_in_finish 用于验证属性包。identity-attributes 库会同时添加这两个方法:Motoko 中的 mo:identity-attributes mixin,以及 Rust 中的 #[identity_attributes] 属性(references/rust-identity-attributes.md)。两者返回相同的 Candid 类型,因此无论使用哪一种,前端都相同。

密钥

requestAttributes({ keys, nonce }) 必须同时提供两者;没有默认的密钥集合。

密钥含义用途
name用户与 II 关联的账户中的显示名称个性化
email用户与 II 关联的账户中的原始地址;II 不会验证此地址联系信息、邮件列表
verified_email仅当 II 确认用户控制该地址时才会提供(OpenID 提供商将其标记为已验证,或用户通过 II 验证了该地址)访问控制;唯一可用于授权的电子邮件地址

同时请求 email 和 verified_email 可实现回退:验证通过时,两者的值相同;否则只会返回 email。

scopedKeys 会将密钥限定到用户用于登录的账户,因此它们会在同一步骤中共享,无需额外提示词:

调用密钥默认 keys
scopedKeys({ openIdProvider: 'google' })openid:https://accounts.google.com:<key>name、email、verified_email
scopedKeys({ openIdProvider: 'apple' })openid:https://appleid.apple.com:<key>同上
scopedKeys({ openIdProvider: 'microsoft' })openid:https://login.microsoftonline.com/{tid}/v2.0:<key>({tid} 为字面值)同上
scopedKeys({ ssoDomain: 'acme.com' })sso:acme.com:<key>name、email(不含 verified_email)

传入 keys 可缩小范围,例如:scopedKeys({ openIdProvider: 'google', keys: ['name', 'verified_email'] })。

前端
import { AuthClient } from "@icp-sdk/auth/client";
import { AttributesIdentity } from "@icp-sdk/core/identity";
import { HttpAgent, Actor } from "@icp-sdk/core/agent";
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
import { Principal } from "@icp-sdk/core/principal";

const II_PRINCIPAL = "rdmx6-jaaaa-aaaaa-aaadq-cai";
const rootKey = safeGetCanisterEnv()?.IC_ROOT_KEY;

async function signInWithAttributes(authClient, canisterId, idl) {
  // Anonymous handle, used only to mint the nonce.
  const anonymousActor = Actor.createActor(idl, { agent: await HttpAgent.create({ rootKey }), canisterId });

  // In parallel, one interaction for the user. `nonce` is a function the client
  // calls when it needs the value, so the request is in flight while II opens.
  const signInPromise = authClient.signIn();
  const attributesPromise = authClient.requestAttributes({
    keys: ["name", "verified_email"],
    nonce: () => anonymousActor._internet_identity_sign_in_start(),
  });

  const identity = await signInPromise;
  const attributes = await attributesPromise;

  // The bundle travels as sender_info on each call made with this identity.
  const verifiedAgent = await HttpAgent.create({
    identity: new AttributesIdentity({
      inner: identity,
      attributes,
      signer: { canisterId: Principal.fromText(II_PRINCIPAL) },
    }),
    rootKey,
  });
  const verifiedActor = Actor.createActor(idl, { agent: verifiedAgent, canisterId });

  const result = await verifiedActor._internet_identity_sign_in_finish();
  if ("err" in result) {
    throw new Error(`Attribute verification failed: ${JSON.stringify(result.err)}`);
  }
  return identity;
}

如需一键登录,请使用 openIdProvider 或 ssoDomain 构建客户端,在 @icp-sdk/auth/client 导入中添加 scopedKeys,并改为请求 scopedKeys({ openIdProvider: "google", keys: ["name", "verified_email"] }) 或 scopedKeys({ ssoDomain: "acme.com" });其余内容不变。

每个捆绑包都带有三个隐式字段,后端必须验证:implicit:nonce(由后端签发且尚未使用的 nonce)、implicit:origin(受信任的前端来源)、implicit:issued_at_timestamp_ns(时间新鲜,通常在五分钟内)。

后端:Motoko
[dependencies]
identity-attributes = "0.4.1"
core                = "2.5.0"

[toolchain]
moc = "1.6.0"

include IdentityAttributes({ onVerified }) 会注入这两个方法,并且仅在捆绑包通过签名者、来源、nonce 和时效性检查后才运行 onVerified。它会将捆绑包解析为 { name : ?Text; email : ?Text; sso : ?Text }:

  • OpenID 和未限定来源:email 来自 verified_email(或 openid:<provider>:verified_email),因此前端需要请求 verified_email。
  • SSO 来源:name 和 email 来自 sso:<domain>:name / sso:<domain>:email,仅当域名列于 trusted_sso_domains 时才会接受,且 sso 为该域名。否则,sso 为 null。
import IdentityAttributes "mo:identity-attributes";
import Map "mo:core/Map";
import Principal "mo:core/Principal";

persistent actor {
  type Profile = { name : ?Text; email : ?Text; sso : ?Text };

  let profiles = Map.empty<Principal, Profile>();

  include IdentityAttributes({
    onVerified = func(caller, attrs) {
      profiles.add(caller, attrs);
    };
  });

  public query func getProfile(userId : Principal) : async ?Profile {
    profiles.get(userId)
  };
};
canisters:
  - name: backend
    settings:
      environment_variables:
        # Required. A local II (`ii: true`) has the same principal.
        trusted_attribute_signers: "rdmx6-jaaaa-aaaaa-aaadq-cai"
        # Required, comma-separated.
        frontend_origins: "https://your-app.icp.net"
        # Optional, comma-separated; omit to reject all sso:* keys.
        trusted_sso_domains: "your-org.com"

未设置 trusted_attribute_signers 时,所有捆绑包都会因不受信任而被拒绝;未设置 frontend_origins 时,_internet_identity_sign_in_finish 会返回 #err(#FrontendOriginsNotConfigured)。这些错误变体(#NoAttributes、#MalformedCandid、#FrontendOriginMismatch、#Stale、#UnknownNonce、#AmbiguousAttribute、#UntrustedSsoSource、#MixedSsoSources)会告知前端应使用新的 nonce 重试,还是报告程序错误。

在多个来源提供应用

II 会按来源派生主体,因此 https://<canister-id>.icp.net 和 https://shop.example.com 对应两个不同的用户。选择一个派生来源(Canister 地址:自定义域名可能变更,Canister 地址不会),并将其他来源列为备选来源。请在某个来源有用户之前固定该设置:如果某个已有登录记录的来源被重新指向,基于该来源创建的所有账户都会变得无法访问。

  1. 备选来源设置 derivationOrigin;主来源不设置:

    const authClient = new AuthClient({ derivationOrigin: "https://<canister-id>.icp.net" });
    
  2. 派生来源提供 /.well-known/ii-alternative-origins(在静态站点构建中为 dir/.well-known/ii-alternative-origins):

    { "alternativeOrigins": ["https://shop.example.com"] }
    

    最多 100 项,且只能填写来源(不带末尾斜杠和路径)。超过上限时,II 会拒绝整个列表(“has too many entries: To prevent misuse at most 100 alternative origins are allowed”):所有备选来源都会停止验证身份,而不只是超出的项。

  3. 使用 @dfinity/static-site 时,添加 _headers 区块(该文件没有扩展名,且经过认证的资源默认不会设置 CORS 标头):

    /.well-known/ii-alternative-origins
      Content-Type: application/json
      Access-Control-Allow-Origin: *
    

在兄弟子域名之间共享一次登录的实现方式基于此功能:references/shared-sessions.md。

登录屏幕上的应用元数据

在派生来源提供 /.well-known/ii-app-metadata({ "name", "description", "logo" },所有字段均可选),并为该文档和徽标启用 CORS,II 就会在来源旁显示应用名称、标语和徽标。name 最多 40 个码点,description 最多 120 个码点;logo 应为指向同源栅格图像的相对 URL(不支持 SVG)。任一字段无效都会导致整个文档失效。完整规则:references/app-metadata.md。

后端:访问控制

拒绝匿名主体、角色守卫,以及在异步函数中绑定调用者,并非 II 特有功能:请参阅 canister-security 技能。

评论

支持 Markdown