@innerfire/auth-sdk
v0.3.0
Published
面向多租户 SaaS 应用的鉴权 SDK,提供登录、会话管理、权限校验、自动续期、跨项目恢复等能力。SDK 统一使用 Cookie 会话模式,内置 React 集成、Axios 拦截器与服务端 sid 恢复适配。
Readme
@innerfire/auth-sdk
面向多租户 SaaS 应用的鉴权 SDK,提供登录、会话管理、权限校验、自动续期、跨项目恢复等能力。SDK 统一使用 Cookie 会话模式,内置 React 集成、Axios 拦截器与服务端 sid 恢复适配。
安装
npm install @innerfire/auth-sdk
# peer dependencies
npm install react antd axios快速开始
import {
createAuthClient,
AuthProvider,
LoginCard,
useAuth,
} from "@innerfire/auth-sdk/react";
import { setupAuthInterceptor } from "@innerfire/auth-sdk/interceptors";
const client = createAuthClient({
baseURL: "https://auth.example.com",
tenant: "my-org",
projectId: "admin",
});
const api = axios.create({ baseURL: "https://api.example.com" });
setupAuthInterceptor(api, {
client,
onUnauthorized: () => {
window.location.href = "/login";
},
});
function App() {
return (
<AuthProvider client={client}>
<Dashboard />
</AuthProvider>
);
}
function Dashboard() {
const { session, tenantContext, logout } = useAuth();
if (!session) {
return <LoginCard client={client} />;
}
return (
<div>
<p>用户: {tenantContext?.userId}</p>
<p>角色: {tenantContext?.roles.join(", ")}</p>
<button onClick={logout}>退出</button>
</div>
);
}鉴权流程
登录
用户提交凭证
↓
POST /auth/login { username, password, provider, projectId }
↓
服务端验证 → 返回 { sessionId, projectToken }
↓
SDK 存储 sessionId(cookie)和 projectToken(sessionStorage)
↓
AuthProvider 更新上下文 → 触发 UI 重渲染projectId 由 createAuthClient() 的固定配置提供,LoginCard 不再单独接收 projectId。
会话校验
页面加载时,AuthProvider 自动调用 GET /auth/session 检查会话:
- 会话有效 → 校准当前项目 token → 拉取租户上下文 (
GET /me/tenant-context) - 会话无效/过期 → 清除存储,展示未登录状态
自动续期
Axios 拦截器在收到 401 响应时自动处理:
| 错误码 | 处理方式 |
| ------------------------------------------------- | ------------------------------------------------------------------------------- |
| SESSION_EXPIRED | 调用 POST /auth/refresh 续期后重试请求 |
| PROJECT_TOKEN_EXPIRED / PROJECT_TOKEN_INVALID | 调用 changeToken() 重新换取当前项目 token,网络异常时按配置有限重试后重放请求 |
若最终仍无法恢复,SDK 会清除会话并触发 onUnauthorized 回调。
项目令牌切换
client.changeToken(projectId?)
↓
POST /projects/:id/access-tickets → 获取临时票据
↓
POST /projects/:id/token-exchange → 用票据换 projectToken
↓
存储新的 projectToken 和 projectId- 传入
projectId时:为目标项目换取新 token - 不传时:回退到
createAuthClient()中配置的固定projectId
跨项目跳转
当用户从项目 A 跳转到项目 B 时,auth-sdk 支持通过 URL 传递 sid 来恢复会话,无需重新登录。
项目 A(来源项目):
const sid = client.getSessionId();
window.location.href = `${targetURL}?sid=${encodeURIComponent(sid ?? "")}`;项目 B(目标项目,客户端恢复):
import { createAuthClient, AuthProvider } from '@innerfire/auth-sdk/react';
const client = createAuthClient({
baseURL: 'https://auth.example.com',
tenant: 'my-org',
projectId: 'project-b',
});
function App() {
return (
<AuthProvider client={client}>
{/* 你的应用 */}
</AuthProvider>
);
}客户端工作流程:
- 用户从项目 A 跳转到项目 B,URL 携带
?sid=<sessionId> - 项目 B 的
AuthProvider初始化时自动检测 URL 中的sid - SDK 将
sid写入 cookie 会话存储 - SDK 清理 URL 中的
sid参数 - 会话校验成功后,SDK 自动调用
changeToken(projectId)获取项目 B 的 project token - 用户以已登录状态进入项目 B
SSR / Middleware 适配:
如果目标应用需要在服务端首屏前恢复会话,可以使用 @innerfire/auth-sdk/middleware 提供的 sid 恢复辅助函数:
import { createSidResumeResponse } from "@innerfire/auth-sdk/middleware";
export function middleware(request: NextRequest) {
const result = createSidResumeResponse(request.url, {
cookieName: "auth_session",
secure: true,
sameSite: "lax",
});
if (!result) {
return NextResponse.next();
}
const response = NextResponse.redirect(result.location);
response.headers.set("set-cookie", result.setCookieHeader);
return response;
}这类服务端适配的推荐流程是:
- 在服务端检测 URL 中的
sid - 生成写入 session cookie 的
Set-Cookie - 重定向到移除
sid后的 URL - 页面进入客户端后,
AuthProvider再自动校准当前项目 token
注意事项:
sid通过 URL 传递,必须使用 HTTPS- SDK 会在消费后立即清理 URL,避免
sid泄露 - 如果
sid无效或已过期,SDK 会清理会话并按未登录态处理 - 目标项目必须配置固定
projectId才能自动恢复并换取项目 token
退出登录
client.logout()
↓
POST /auth/logout
↓
清除 sessionId、projectToken、projectId
↓
AuthProvider 上下文重置为未登录状态子路径导出
SDK 按功能拆分为独立子路径,支持 tree-shaking:
| 子路径 | 内容 |
| ---------------------------------- | ----------------------------------------------------------------------- |
| @innerfire/auth-sdk/core | AuthClient、类型定义、存储、sid 工具 |
| @innerfire/auth-sdk/react | AuthProvider、useAuth、LoginCard、SessionProvider、useSession |
| @innerfire/auth-sdk/server | createAuthServer,服务端鉴权(SSR / 纯后端) |
| @innerfire/auth-sdk/interceptors | Axios 请求/响应拦截器 |
| @innerfire/auth-sdk/middleware | 服务端路由守卫、sid 恢复辅助函数 |
| @innerfire/auth-sdk | 全量导出 |
模块详解
core — 创建客户端
import { createAuthClient } from "@innerfire/auth-sdk/core";
const client = createAuthClient({
baseURL: "https://auth.example.com",
tenant: "my-org",
projectId: "admin",
cookieName: "auth_session",
cookieOptions: {
path: "/",
secure: true,
sameSite: "lax",
},
});配置说明:
| 字段 | 说明 |
| --------------- | ------------------------------------- |
| baseURL | 认证 API 地址,默认同源 |
| tenant | 租户标识,必填 |
| projectId | 当前应用固定项目 ID,必填 |
| cookieName | 会话 cookie 名称,默认 auth_session |
| cookieOptions | 会话 cookie 属性 |
AuthClient 方法:
| 方法 | 说明 |
| ------------------------- | ------------------------------------------------------- |
| login(request) | 登录,自动使用配置中的 projectId 建立会话和项目 token |
| logout() | 退出,清除存储 |
| getSession() | 获取当前会话,无会话返回 null |
| refreshSession() | 刷新会话 |
| getTenantContext() | 获取租户上下文 |
| getProviders() | 获取可用登录方式 |
| changeToken(projectId?) | 为目标项目或默认项目重新换取 project token |
| getSessionId() | 同步读取 sessionId |
| setSessionId(sessionId) | 同步写入 sessionId |
| getProjectToken() | 同步读取 projectToken |
| getCurrentProjectId() | 同步读取当前项目 ID |
| clearSession() | 清除所有会话数据 |
| getConfig() | 获取配置 |
react — React 集成
AuthProvider
import { AuthProvider } from "@innerfire/auth-sdk/react";
<AuthProvider client={client}>{children}</AuthProvider>;管理会话状态和租户上下文,挂载时自动校验会话,并在需要时自动校准当前项目 token。
useAuth()
const {
session,
tenantContext,
isLoading,
error,
client,
login,
logout,
refreshSession,
} = useAuth();LoginCard
支持两种模式:
SPA 模式(直接调用 AuthClient):
import { LoginCard } from "@innerfire/auth-sdk/react";
<LoginCard
client={client}
onSuccess={(sessionId, projectToken) => router.push("/dashboard")}
/>;SSR 模式(POST 到自己的 API route):
<LoginCard
action="/api/auth/login"
onSuccess={() => router.push("/dashboard")}
/>action 模式下,LoginCard 会向指定 URL 发送 POST 请求,并从同级路径 /api/auth/providers 自动获取登录方式。也可通过 providers prop 直接传入,跳过自动获取。
基于 Ant Design 实现,自动支持多 provider tab 切换(如账号密码 + SSO)。
SessionProvider + useSession(SSR 模式)
SSR 框架中,由服务端解析 session 后通过 SessionProvider 传入,客户端组件通过 useSession 消费,无需在前端持有 Auth Center 配置。
// app/layout.tsx(Server Component)
import { cookies } from "next/headers";
import { auth } from "@/lib/auth";
import { SessionProvider } from "@innerfire/auth-sdk/react";
export default async function RootLayout({ children }) {
const sid = cookies().get("auth_session")?.value;
const data = sid ? await auth.getSession(sid) : null;
return (
<SessionProvider
session={data?.session ?? null}
tenantContext={data?.tenantContext ?? null}
>
{children}
</SessionProvider>
);
}// app/dashboard/page.tsx(Client Component)
"use client";
import { useSession } from "@innerfire/auth-sdk/react";
export default function Dashboard() {
const { session, tenantContext, status } = useSession();
if (status === "unauthenticated") redirect("/login");
return <div>欢迎,{tenantContext?.userId}</div>;
}server — 服务端鉴权
createAuthServer() 用于 SSR 框架(Next.js、Nuxt 等)和纯后端 API 服务,直接与 Auth Center 通信,无跨域限制。
// lib/auth.ts
import { createAuthServer } from "@innerfire/auth-sdk/server";
export const auth = createAuthServer({
baseURL: process.env.AUTH_CENTER_URL!,
tenant: process.env.TENANT_ID!,
projectId: process.env.PROJECT_ID!,
secret: process.env.AUTH_SECRET, // 可选:服务间通信密钥
});AuthServer 方法:
| 方法 | 说明 |
| --------------------- | -------------------------------------------------------------------- |
| login(credentials) | 登录,返回 sessionId、projectToken、setCookieHeader |
| verifySession(sid) | 验证 session 是否有效,返回 Session 或 null |
| getSession(sid) | 获取 session + tenantContext,用于 SSR layout |
| logout(sid) | 登出 |
| refreshSession(sid) | 刷新 session |
| getProviders() | 获取可用登录方式 |
| resumeFromUrl(url) | 跨项目 sid resume:提取 sid → 换 token → 返回 cookie 指令 + 干净 URL |
Next.js API Route 示例:
// app/api/auth/login/route.ts
import { auth } from "@/lib/auth";
export async function POST(req: Request) {
const body = await req.json();
const result = await auth.login(body);
return new Response(
JSON.stringify({
sessionId: result.sessionId,
projectToken: result.projectToken,
}),
{
headers: {
"Content-Type": "application/json",
"Set-Cookie": result.setCookieHeader,
},
},
);
}Next.js Middleware 路由守卫 + 跨项目 sid resume:
// middleware.ts
import { auth } from "@/lib/auth";
export async function middleware(req) {
// 跨项目跳转:URL 带 ?sid=xxx 时自动换 token 并重定向
const resumeResult = await auth.resumeFromUrl(req.url);
if (resumeResult) {
return new NextResponse(null, {
status: 302,
headers: {
Location: resumeResult.location,
"Set-Cookie": resumeResult.setCookieHeader,
},
});
}
// 正常鉴权
const sid = req.cookies.get("auth_session")?.value;
if (!sid) return NextResponse.redirect(new URL("/login", req.url));
const session = await auth.verifySession(sid);
if (!session) return NextResponse.redirect(new URL("/login", req.url));
}Express 中间件示例:
import { auth } from "./lib/auth";
app.use(async (req, res, next) => {
const token = req.headers["x-project-token"];
if (!token) return res.status(401).json({ error: "Unauthorized" });
// 也可用 verifySession 校验 cookie session
next();
});interceptors — Axios 拦截器
import axios from "axios";
import { setupAuthInterceptor } from "@innerfire/auth-sdk/interceptors";
const api = axios.create({ baseURL: "https://api.example.com" });
const cleanup = setupAuthInterceptor(api, {
client,
autoRefresh: true,
changeTokenRetryCount: 1,
onUnauthorized: () => {
window.location.href = "/login";
},
});
// 卸载时调用 cleanup() 移除拦截器自动注入的请求头:
| Header | 值 |
| ----------------- | ------------------------- |
| X-Tenant-ID | 配置中的 tenant |
| X-Project-Token | 当前 projectToken(如有) |
middleware — 服务端路由守卫
import {
createAuthGuard,
createSidResumeResponse,
} from "@innerfire/auth-sdk/middleware";
const guard = createAuthGuard({
client,
projectAccess: {
projectId: "proj-123",
resource: "articles",
action: "read",
},
onUnauthorized: () => {
redirect("/login");
},
onForbidden: () => {
redirect("/403");
},
});
export async function middleware(request: NextRequest) {
const sidResume = createSidResumeResponse(request.url, {
secure: true,
sameSite: "lax",
});
if (sidResume) {
const response = NextResponse.redirect(sidResume.location);
response.headers.set("set-cookie", sidResume.setCookieHeader);
return response;
}
return guard();
}守卫流程:校验会话 → 获取租户上下文 → 可选校验项目权限 (POST /authz/check) → 可选获取数据范围 (POST /authz/query-scopes)
错误处理
SDK 统一抛出 AuthError,携带结构化错误码:
import { AuthError, AuthErrorCode } from "@innerfire/auth-sdk";
try {
await client.login({
username: "admin",
password: "123",
provider: "password",
});
} catch (error) {
if (error instanceof AuthError) {
switch (error.code) {
case AuthErrorCode.INVALID_CREDENTIALS:
break;
case AuthErrorCode.SESSION_EXPIRED:
break;
case AuthErrorCode.PROJECT_TOKEN_EXPIRED:
break;
}
if (error.relogin) {
// 需要重新登录
}
}
}错误码列表:
| 错误码 | 说明 |
| ------------------------ | ---------------- |
| VALIDATION_ERROR | 请求参数校验失败 |
| INVALID_CREDENTIALS | 用户名或密码错误 |
| SESSION_EXPIRED | 会话已过期 |
| SESSION_REVOKED | 会话已被撤销 |
| PROJECT_NOT_ENABLED | 项目未启用 |
| NO_PROJECT_ACCESS | 无项目访问权限 |
| PROJECT_TOKEN_EXPIRED | 项目令牌过期 |
| PROJECT_TOKEN_INVALID | 项目令牌无效 |
| TENANT_CONTEXT_INVALID | 租户上下文无效 |
类型定义
所有类型通过 Zod schema 定义,同时导出 schema 和 TS 类型:
import type {
AuthClientConfig,
LoginRequest,
LoginResponse,
Session,
TenantContext,
LoginProvider,
CookieOptions,
AuthErrorCode,
} from "@innerfire/auth-sdk";