授權系統,具備基於角色的存取控制。所有管理個人或受限制資料的應用程式必備。
授權
授權擴充套件,用於 Caffeine AI。
概述
此技能透過 mixin 模式新增一個具備角色存取控制的驗證與授權系統。MixinAuthorization mixin 會自動提供標準的授權端點。
後端
具備角色存取控制的驗證系統。
有一個預先建置好的函式庫 mo:caffeineai-authorization/access-control.mo,提供核心驗證與角色存取控制。
模組 API
module {
public type UserRole = {
#admin;
#user;
#guest;
};
public type AccessControlState = { /* 內部狀態 */ };
public func initState() : AccessControlState;
public func getUserRole(state : AccessControlState, caller : Principal) : UserRole;
public func assignRole(state : AccessControlState, caller : Principal, user : Principal, role : UserRole);
public func isAdmin(state : AccessControlState, caller : Principal) : Bool;
public func hasPermission(state : AccessControlState, caller : Principal, requiredRole : UserRole) : Bool;
};
初始化由 MixinAuthorization 內部處理——請勿直接呼叫 initialize。第一個登入的已驗證使用者會自動成為管理員;不需要 token 或密鑰。
重要:include MixinAuthorization(accessControlState) 這行必須放在 main.mo 中,而不是自訂的 mixin 檔案。
選擇退出 / 自訂驗證
如果使用者要求用自訂驗證(例如使用者名稱/密碼驗證、不使用 Internet Identity、或不使用 MixinAuthorization)取代此授權系統,請從專案中移除 caffeineai-authorization mops 套件。這是移除 include-authorization lint 要求的唯一支援方式,因為該 lint 規則是由套件提供的。請勿加入抑制註解,或在安裝套件的情況下省略 include MixinAuthorization(accessControlState)。
移除套件時,也請移除所有 mo:caffeineai-authorization/* 匯入、accessControlState 初始化、include MixinAuthorization(accessControlState),以及任何屬於此元件的 AccessControl 防護呼叫。並以使用者要求的自訂驗證與授權檢查取代。
在 main.mo 中的設定
import Map "mo:core/Map";
import Principal "mo:core/Principal";
import AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
import Types "types";
import ProfileMixin "mixins/Profile";
actor {
let accessControlState = AccessControl.initState();
include MixinAuthorization(accessControlState, null);
let userProfiles = Map.empty<Principal, Types.UserProfile>();
include ProfileMixin(accessControlState, userProfiles);
};
types.mo 中的型別定義
module {
public type UserProfile = {
name : Text;
};
};
自訂 Mixin 範例 (mixins/Profile.mo)
前端需要 getCallerUserProfile、saveCallerUserProfile 和 getUserProfile。將 accessControlState 傳遞給您的 mixin,以便檢查權限。
import Map "mo:core/Map";
import Principal "mo:core/Principal";
import Runtime "mo:core/Runtime";
import AccessControl "mo:caffeineai-authorization/access-control";
import Types "../types";
mixin (
accessControlState : AccessControl.AccessControlState,
userProfiles : Map.Map<Principal, Types.UserProfile>,
) {
public query ({ caller }) func getCallerUserProfile() : async ?Types.UserProfile {
if (not AccessControl.hasPermission(accessControlState, caller, #user)) {
Runtime.trap("未授權");
};
userProfiles.get(caller);
};
public shared ({ caller }) func saveCallerUserProfile(profile : Types.UserProfile) : async () {
if (not AccessControl.hasPermission(accessControlState, caller, #user)) {
Runtime.trap("未授權");
};
userProfiles.add(caller, profile);
};
public query ({ caller }) func getUserProfile(user : Principal) : async ?Types.UserProfile {
if (caller != user and not AccessControl.isAdmin(accessControlState, caller)) {
Runtime.trap("未授權:只能檢視自己的個人資料");
};
userProfiles.get(user);
};
};
防護模式
對每個公開函式套用適當的防護:
// 僅管理員:
if (not AccessControl.hasPermission(accessControlState, caller, #admin)) {
Runtime.trap("未授權:只有管理員可以執行此操作");
};
// 僅使用者:
if (not AccessControl.hasPermission(accessControlState, caller, #user)) {
Runtime.trap("未授權:只有使用者可以執行此操作");
};
// 任何使用者(包括訪客):無需檢查
設計指南
- 匿名主體視為訪客。
assignRole內部已包含僅管理員的防護。- 修改資料的已驗證端點使用
shared({ caller })。 - 擷取資料的已驗證端點使用
query({ caller })。 - 必要時處理所有權驗證。
- 授權失敗時使用
Runtime.trap。
電子郵件屬性
MixinAuthorization 可以在登入時擷取使用者已驗證的 Internet Identity 屬性(姓名和電子郵件)。將回呼作為第二個參數傳遞,而非 null;該回呼在每次登入後、屬性套件驗證完成時執行一次。
請勿直接使用 mo:identity-attributes mixin——一律透過 MixinAuthorization。回呼接收呼叫者主體和已驗證的屬性:
{
name : ?Text; // 已驗證的顯示名稱(若存在)
email : ?Text; // 一律為已驗證的地址——來自 II 的 `verified_email`,絕非未驗證的 `email` 鍵
sso : ?Text; // 若身分來自 SSO,則為 SSO 網域,否則為 null
}
欄位名稱為 email,但只會包含 II 的 verified_email 值——從不讀取未驗證的 email 鍵。在回呼中使用 attrs.email(沒有 attrs.verified_email 欄位)。
將它們儲存在您自己的狀態中,並公開一個 getter 來讀取:
import Map "mo:core/Map";
import Principal "mo:core/Principal";
import AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
actor {
let accessControlState = AccessControl.initState();
let emails = Map.empty<Principal, Text>();
include MixinAuthorization(
accessControlState,
?(func(caller : Principal, attrs : { name : ?Text; email : ?Text; sso : ?Text }) {
switch (attrs.email) {
case (?email) { emails.add(caller, email) };
case null {};
};
}),
);
public query ({ caller }) func getCallerEmail() : async ?Text {
emails.get(caller);
};
};
屬性驗證所需的 trusted_attribute_signers 和 frontend_origins 容器環境變數由 Caffeine 平台自動設定——您無需設定。
在前端擷取電子郵件
登入後,像其他已驗證的 actor 方法一樣查詢 getter:
const { data: callerEmail } = useQuery<string | null>({
queryKey: ['callerEmail'],
queryFn: () => actor.getCallerEmail(),
enabled: !!actor && isAuthenticated,
});
前端
具備角色存取控制的驗證系統。
使用者個人資料設定
使用 Internet Identity 時,使用者只有在登入後才會取得主體 ID。匿名主體視為訪客。主體 ID 不易閱讀——當使用者首次以新主體登入時,請詢問他們的名稱。
個人資料的後端 API:
getCallerUserProfile(): Promise<UserProfile | null>—— 若無個人資料則回傳nullsaveCallerUserProfile(profile: UserProfile): Promise<void>—— 儲存名稱和個人資料getUserProfile(user: Principal): Promise<UserProfile | null>—— 擷取其他使用者的個人資料
規則:
- 登入時,若使用者已有個人資料,請勿再次詢問名稱
- 顯示使用者的個人資料名稱,而非主體 ID
- 確保使用者在看到任何應用程式資料前必須先登入
- 登出時,清除所有快取的應用程式資料,包括快取的使用者個人資料
防止個人資料設定彈窗閃爍
export function useGetCallerUserProfile() {
const { actor, isFetching: actorFetching } = useActor();
const query = useQuery<UserProfile | null>({
queryKey: ['currentUserProfile'],
queryFn: async () => {
if (!actor) throw new Error('Actor 不可用');
return actor.getCallerUserProfile();
},
enabled: !!actor && !actorFetching,
retry: false,
});
return {
...query,
isLoading: actorFetching || query.isLoading,
isFetched: !!actor && query.isFetched,
};
}
然後在您的元件中:
const showProfileSetup = isAuthenticated && !profileLoading && isFetched && userProfile === null;
驗證狀態生命週期
useInternetIdentity 鉤子暴露兩種狀態——請使用正確的狀態:
| 情境 | loginStatus |
isAuthenticated |
|---|---|---|
| 頁面載入,無儲存的工作階段 | "idle" |
false |
| 頁面載入,還原儲存的工作階段 | "initializing" |
false → true |
| 重新載入後還原儲存的工作階段 | "idle" |
true |
| 互動式登入進行中(彈出視窗開啟) | "logging-in" |
false |
| 互動式登入剛完成 | "success" |
true |
| 登入彈出視窗失敗/取消 | "loginError" |
false |
重要: isLoginSuccess(loginStatus === "success")僅在透過彈出視窗進行互動式登入後為 true。在頁面重新載入還原已儲存身分時不會為 true。切勿使用 isLoginSuccess 來判斷已驗證與未驗證的 UI——一律使用 isAuthenticated。
登入按鈕的關鍵狀態:
isInitializing——AuthClient正在從 IndexedDB 載入;停用按鈕以防止在客戶端準備好之前點擊。isLoggingIn—— II 彈出視窗已開啟;停用按鈕以防止重複彈出視窗。
登入元件
import { useInternetIdentity } from '@caffeineai/core-infrastructure';
import { useQueryClient } from '@tanstack/react-query';
export default function LoginButton() {
const { login, clear, isAuthenticated, isInitializing, isLoggingIn } = useInternetIdentity();
const queryClient = useQueryClient();
const handleAuth = () => {
if (isAuthenticated) {
clear();
queryClient.clear();
} else {
login();
}
};
return (
<button
onClick={handleAuth}
disabled={isInitializing || isLoggingIn}
className={`px-6 py-2 rounded-full transition-colors font-medium ${
isAuthenticated
? 'bg-gray-200 hover:bg-gray-300 text-gray-800'
: 'bg-blue-600 hover:bg-blue-700 text-white'
} disabled:opacity-50`}
>
{isInitializing ? '載入中...' : isAuthenticated ? '登出' : '登入'}
</button>
);
}
login() 和 clear() 函式是 fire-and-forget(它們不會回傳追蹤完整流程的 Promise)。鉤子的 isLoggingIn / isInitializing 狀態會追蹤非同步生命週期——請勿將它們包裝在本機的 useState / isPending 邏輯中。
使用 isAuthenticated 來控制已驗證 UI 的顯示(涵蓋新登入和頁面重新載入後的還原工作階段):
{isAuthenticated ? (
<AuthenticatedApp />
) : (
<LoginScreen />
)}
比較目前使用者與資料作者
import { useInternetIdentity } from '@caffeineai/core-infrastructure';
import type { Principal } from '@icp-sdk/core/principal';
const { identity } = useInternetIdentity();
const isAuthor = (authorPrincipal: Principal): boolean => {
if (!identity) return false;
return authorPrincipal.toString() === identity.getPrincipal().toString();
};
存取控制 UI
對於僅管理員或個人應用程式,當未授權使用者嘗試存取應用程式時,顯示 AccessDeniedScreen 元件。
錯誤處理
在前端優雅地處理來自後端 Debug.trap 呼叫的授權錯誤,並向使用者顯示適當的錯誤訊息。
注意:第一個管理員的初始化由 @caffeineai/core-infrastructure 自動完成。第一個登入的已驗證使用者會成為管理員;不需要 token 或密鑰。






