aptx-api-plugin-auth
npx skills add https://github.com/haibaraaiaptx/aptx-skill --skill aptx-api-plugin-auth
Agent 安装分布
Skill 文档
aptx-api-plugin-auth
ç®å½
- 工使µç¨
- TokenStore æ¥å£
- ä¸é´ä»¶æ¨¡å¼ï¼æ¨èï¼
- æ§å¶å¨æ¨¡å¼ï¼æå¨ token 管çï¼
- é ç½®é项
- é误å¤ç
- å ³é®çº¦æ
- é«çº§ä¸»é¢
工使µç¨
卿¥å ¥è®¤è¯æä»¶æ¶ï¼æ§è¡ä»¥ä¸æµç¨ï¼
- 强å¶ä½¿ç¨
TokenStoreæ½è±¡ï¼ä¸ç´æ¥å¨ä¸å¡ä»£ç éæ£è½ token 读åã - å建
createAuthMiddleware({ store, refreshToken, ... })å¹¶æå°RequestClient.use(...)ã refreshTokenè¿å{ token, expiresAt }æstringï¼ä¼å è¿åexpiresAt以便åå¨å±åæ¥è¿ææ¶é´ã- 使ç¨
shouldRefreshå®ä¹è§¦åå·æ°æ¡ä»¶ï¼é»è®¤ä» å¨HttpError(401)æ¶å·æ°ã - é
ç½®
onRefreshFailedå¤çå·æ°å¤±è´¥åçä¸å¡å¨ä½ï¼ä¾å¦æ¸ çä¼è¯æè·³è½¬ç»å½ï¼ã
é»è®¤æ¨èæé ï¼
@aptx/token-storeï¼TokenStore æ¥å£å®ä¹ï¼@aptx/token-store-cookieï¼æµè§å¨ cookie å®ç°ï¼- SSRï¼
@aptx/token-store-ssr-cookieï¼Node/SSR request-scoped cookie å®ç°ï¼
TokenStore æ¥å£
store åæ°æ¯æä¸ç§å½¢å¼ï¼TokenStoreResolver ç±»åï¼ï¼
type TokenStoreResolver =
| TokenStore // ç´æ¥å®ä¾ï¼ååå
¼å®¹ï¼
| (() => TokenStore) // 忥工å彿°
| (() => Promise<TokenStore>); // 弿¥å·¥å彿°ï¼SSRï¼
TokenStore æ¥å£ï¼ç± @aptx/token-store æä¾ï¼ï¼
interface TokenStore {
// è·ååå¨ç token
getToken(): string | undefined | Promise<string | undefined>;
// åå¨ tokenï¼å¯é meta ç¨äºæ¯æè¿ææ¶é´åå
¶ä»å
æ°æ®ï¼
setToken(token: string, meta?: { expiresAt?: number; [key: string]: unknown }): void | Promise<void>;
// æ¸
é¤ token
clearToken(): void | Promise<void>;
// --- å¯éæ©å±æ¹æ³ ---
// è·åå
æ°æ®ï¼ç¨äºè¯»å expiresAtï¼
getMeta?(): Promise<{ expiresAt?: number } | undefined>;
// è·å宿´è®°å½ï¼token + metaï¼
getRecord?(): Promise<{ token: string; meta?: { expiresAt?: number } } | undefined>;
}
使ç¨åºæ¯
| å½¢å¼ | åºæ¯ | 说æ |
|---|---|---|
| ç´æ¥å®ä¾ | æµè§å¨ç«¯ | å便¨¡å¼ï¼ææè¯·æ±å ±äº«åä¸ store |
| 忥工å彿° | æµè§å¨ç«¯ï¼æ¨èï¼ | ä¿æ API ä¸è´æ§ |
| 弿¥å·¥å彿° | SSR 端 | æ¯è¯·æ±ç¬ç« storeï¼ä»è¯·æ±ä¸ä¸æè¯»å cookie |
æ¨èå®ç°ï¼
@aptx/token-store-cookie– æµè§å¨ cookie åå¨ï¼èªå¨è·¨æ ç¾é¡µåæ¥ï¼@aptx/token-store-ssr-cookie– SSR cookie åå¨ï¼æ¯è¯·æ±ç¬ç«ï¼- èªå®ä¹å®ç°ï¼å¦ localStorageãsessionStorageãIndexedDBï¼
ä¸é´ä»¶æ¨¡å¼ï¼æ¨èï¼
æµè§å¨ç«¯
æå°æ¨¡æ¿ï¼
import { createAuthMiddleware } from "@aptx/api-plugin-auth";
import { createCookieTokenStore } from "@aptx/token-store-cookie";
const store = createCookieTokenStore({
tokenKey: "aptx_token",
metaKey: "aptx_token_meta",
syncExpiryFromMeta: true,
});
const auth = createAuthMiddleware({
store, // æä½¿ç¨å·¥å彿°: store: () => store
refreshLeewayMs: 60_000,
refreshToken: async () => {
return { token: "new-token", expiresAt: Date.now() + 30 * 60 * 1000 };
},
});
SSR 端ï¼Next.js App Routerï¼
SSR ç¯å¢éè¦ä¸ºæ¯ä¸ªè¯·æ±å建ç¬ç«ç storeï¼å 为 cookie éè¦ä»è¯·æ±ä¸ä¸æè¯»åã
éè¦ï¼æå¡ç«¯é»è®¤ä¸å¤ç token å·æ°ï¼å·æ°ç±å®¢æ·ç«¯å¤çã
import { cookies } from "next/headers";
import { createAuthMiddleware } from "@aptx/api-plugin-auth";
import { createSsrCookieTokenStore } from "@aptx/token-store-ssr-cookie";
// å建请æ±çº§å«ç API 客æ·ç«¯
export async function createServerApiClient() {
const cookieStore = await cookies();
const auth = createAuthMiddleware({
// 弿¥å·¥å彿°ï¼æ¯æ¬¡è¯·æ±å建ç¬ç« store
store: async () => createSsrCookieTokenStore({
tokenKey: "aptx_token",
metaKey: "aptx_token_meta",
getCookieHeader: () => cookieStore.toString(),
setCookie: (value) => {
// è§£æ Set-Cookie å¹¶è°ç¨ cookieStore.set()
},
}),
// 注æï¼æå¡ç«¯ä¸éè¦ refreshTokenï¼å·æ°ç±å®¢æ·ç«¯å¤ç
// refreshToken ä¼è¢«å¿½ç¥
});
return createApiClient().use(auth);
}
å ³é®ç¹ï¼
- 使ç¨å¼æ¥å·¥å彿°
async () => store - æ¯æ¬¡è°ç¨ä¸é´ä»¶é½ä¼æ§è¡å·¥å彿°ï¼è·å该请æ±ç store å®ä¾
- ä¸å请æ±ç store å®ä¾å®å ¨é离ï¼é¿å token 串ç¨
- æå¡ç«¯ä¸å¤çå·æ°ï¼æ¶å° 401 ç´æ¥æåºé误ï¼ç±å®¢æ·ç«¯å¤ç跳转ç»å½
æ§å¶å¨æ¨¡å¼ï¼æå¨ token 管çï¼
对äºéè¦æå¨æ§å¶ token çåºæ¯ï¼å¦æå¨å·æ°ãé¢å è½½ tokenï¼ï¼
import { createAuthController } from "@aptx/api-plugin-auth";
const controller = createAuthController({
store,
refreshToken: async () => ({ token: "...", expiresAt: Date.now() + 30000 }),
});
// æå¨è§¦åå·æ°å¹¶è¿åæ° token
const token = await controller.refresh();
// ç¡®ä¿è¿åææ tokenï¼æ¥è¿è¿ææ¶èªå¨å·æ°ï¼
const validToken = await controller.ensureValidToken();
AuthController æ¹æ³ï¼
refresh(): Promise<string>– 强å¶å·æ° token å¹¶è¿åæ° tokenensureValidToken(): Promise<string>– è¿åææ tokenï¼æ¥è¿è¿ææ¶èªå¨å·æ°
é ç½®é项
AuthPluginOptions
| é项 | ç±»å | å¿ å¡« | é»è®¤å¼ | 说æ |
|---|---|---|---|---|
store |
TokenStoreResolver |
â | – | Token æä¹ åæ½è±¡ï¼æ¯æç´æ¥å®ä¾ã忥工åã弿¥å·¥åï¼ |
refreshToken |
Promise<{token, expiresAt?} | string> |
â | – | å·æ° token ç弿¥å½æ°ï¼ä» 客æ·ç«¯çæï¼ |
refreshLeewayMs |
number |
â | 60_000 |
æåå·æ°çæ¶é´çªå£ï¼æ¯«ç§ï¼ |
shouldRefresh |
(error, req, ctx) => boolean |
â | 401 æ£æ¥ | 夿æ¯å¦éè¦å·æ°çæ¡ä»¶ï¼ä» 客æ·ç«¯çæï¼ |
onRefreshFailed |
(error) => void |
â | – | å·æ°å¤±è´¥åè°ï¼ä» 客æ·ç«¯çæï¼ |
headerName |
string |
â | "Authorization" |
èªå®ä¹ header åç§° |
tokenPrefix |
string |
â | "Bearer " |
Token åç¼ |
maxRetry |
number |
â | 1 |
å·æ°å¤±è´¥åéè¯æ¬¡æ°ï¼ä» 客æ·ç«¯çæï¼ |
注æï¼æå¡ç«¯ï¼SSRï¼ç¯å¢ä¸ï¼refreshTokenãshouldRefreshãonRefreshFailedãmaxRetry ä¼è¢«å¿½ç¥ï¼å 为æå¡ç«¯é»è®¤ä¸å¤ç token å·æ°ã
refreshToken è¿åå¼
refreshToken å¯ä»¥è¿åä¸¤ç§æ ¼å¼ï¼
- æ¨èæ ¼å¼ï¼å å«è¿ææ¶é´ï¼ï¼
return {
token: "new-token",
expiresAt: Date.now() + 30 * 60 * 1000, // 30 åéåè¿æ
};
- ç®åæ ¼å¼ï¼ä» tokenï¼ï¼
return "new-token"; // éç¨äºä¸éè¦è¿ææ¶é´çåºæ¯
ä¼å
è¿åå
å« expiresAt ç对象ï¼ä¾¿äºåå¨å±ï¼å¦ @aptx/token-store-cookieï¼åæ¥è¿ææ¶é´ã
èªå®ä¹ header é ç½®
对äºéæ åè®¤è¯æ¹æ¡ï¼å¦èªå®ä¹ header ææ Bearer åç¼ï¼ï¼
const auth = createAuthMiddleware({
store,
refreshToken: async () => ({ token: "custom-token" }),
headerName: "X-Auth-Token", // èªå®ä¹ header åç§°
tokenPrefix: "", // ç§»é¤ Bearer åç¼
maxRetry: 2, // å¢å éè¯æ¬¡æ°
});
èªå®ä¹ shouldRefresh æ¡ä»¶
é»è®¤ä» å¨ 401 ç¶æç æ¶è§¦åå·æ°ãå¦éèªå®ä¹é»è¾ï¼
const auth = createAuthMiddleware({
store,
refreshToken: async () => ({ token: "..." }),
shouldRefresh: (error, req, ctx) => {
// error: { status?: number, message?: string, code?: string }
return error.status === 401 || error.code === "TOKEN_EXPIRED";
},
});
åæ°è¯´æï¼
error: é误对象ï¼å å«statusï¼HTTPç¶æç ï¼ãmessageï¼éè¯¯æ¶æ¯ï¼ãcodeï¼ä¸å¡é误ç ï¼req: åå§è¯·æ±å¯¹è±¡ctx: ä¸ä¸æå¯¹è±¡
éè¯çç¥
maxRetry æ§å¶å·æ°å¤±è´¥åçéè¯æ¬¡æ°ï¼
maxRetry: 0– å·æ°å¤±è´¥åç«å³æåºé误maxRetry: 1(é»è®¤) – å·æ°å¤±è´¥åéè¯ä¸æ¬¡maxRetry: 2– å·æ°å¤±è´¥åéè¯ä¸¤æ¬¡
å·æ°å¤±è´¥æ¶ä¼èªå¨è°ç¨ store.clearToken() æ¸
餿¬å° tokenã
é误å¤ç
const auth = createAuthMiddleware({
store,
refreshToken: async () => {
const res = await fetch("/api/refresh");
if (!res.ok) throw new Error("Refresh failed");
return await res.json();
},
onRefreshFailed: (error) => {
// å·æ°å¤±è´¥åçä¸å¡å¨ä½ï¼ä»
客æ·ç«¯æ§è¡ï¼
console.error("Auth refresh failed:", error);
// ä¾å¦ï¼è·³è½¬ç»å½é¡µãæ¸
é¤ç¨æ·ç¶æãæ¾ç¤ºç»å½å¼¹çª
window.location.href = "/login";
},
});
注æï¼onRefreshFailed ä»
å¨å®¢æ·ç«¯æ§è¡ãæå¡ç«¯æ¶å° 401 æ¶ä¼ç´æ¥æåºé误ï¼ä¸è°ç¨æ¤åè°ã
å ³é®çº¦æ
storeå¿ å¡«ä¸æ¯å¯ä¸ token æä¹ åå ¥å£ï¼å¿ é¡»å®ç°TokenStoreæ¥å£ãrefreshTokenå°½éè¿åexpiresAtï¼ä¾¿äºåå¨å±å¤çè¿ææ¶é´ã- è¥å·æ°å¤±è´¥ï¼å¿
é¡»ç±
onRefreshFailedç»ä¸å¤çä¸å¡å¨ä½ï¼ä» 客æ·ç«¯çæï¼ã shouldRefreshé»è®¤ä» å¨HttpError(401)æ¶è§¦åå·æ°ï¼å ¶ä»é误ä¸ä¼å·æ°ï¼ä» 客æ·ç«¯çæï¼ã- æå¡ç«¯é»è®¤ä¸å¤ç token å·æ°ï¼æå¡ç«¯æ¶å° 401 ç´æ¥æåºé误ï¼å·æ°ç±å®¢æ·ç«¯å¤çã
使¶ä¸ä½¿ç¨
- éæ API è°ç¨ï¼æ éç¨æ·è®¤è¯çå ¬å¼æ¥å£ä¸éè¦ä½¿ç¨æ¤æä»¶
- æå¡ç«¯å°æå¡ç«¯è®¤è¯ï¼åºä½¿ç¨ API Key æåºç¨çº§åè¯ï¼èéç¨æ· token
- ä»
é䏿¬¡æ§è®¤è¯ï¼å¦æåªéå¨åºç¨å¯å¨æ¶è·å䏿¬¡ tokenï¼æ éèªå¨å·æ°ï¼å¯ç´æ¥ä½¿ç¨
createAuthControllerèä¸æè½½ä¸é´ä»¶
é«çº§ä¸»é¢
è¾¹ç¼æ åµå¤ç
è¯¦è§ edge-cases.mdï¼å å«ï¼
- å¹¶åå·æ°é²æ¢
- Token éªè¯
- ç½ç»é误éè¯
- è·¨æ ç¾é¡µ Token 忥
- æ éå·æ°ä¿æ¤