bknd-session-handling
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSession Handling
会话管理
Manage user sessions in Bknd: token persistence, session checking, auto-renewal, and invalidation.
在Bknd中管理用户会话:令牌持久化、会话检查、自动续期以及失效处理。
Prerequisites
前置条件
- Bknd project with auth enabled ()
bknd-setup-auth - Auth strategy configured and working ()
bknd-login-flow - For SDK: package installed
bknd - For React: package installed
@bknd/react
- 已启用认证功能的Bknd项目()
bknd-setup-auth - 已配置并正常工作的认证策略()
bknd-login-flow - 使用SDK时:已安装包
bknd - 使用React时:已安装包
@bknd/react
When to Use UI Mode
何时使用UI模式
- Viewing JWT configuration in admin panel
- Checking cookie settings
- Testing session expiration
UI steps: Admin Panel > Auth > Configuration > JWT/Cookie settings
- 在管理面板中查看JWT配置
- 检查Cookie设置
- 测试会话过期
UI操作步骤: 管理面板 > 认证 > 配置 > JWT/Cookie设置
When to Use Code Mode
何时使用代码模式
- Implementing session persistence in frontend
- Checking authentication state on page load
- Handling token expiration gracefully
- Implementing auto-refresh patterns
- Server-side session validation
- 在前端实现会话持久化
- 页面加载时检查认证状态
- 优雅处理令牌过期
- 实现自动刷新模式
- 服务端会话验证
How Sessions Work in Bknd
Bknd中的会话工作原理
Bknd uses stateless JWT-based sessions:
- Login - Server creates signed JWT with user data, returns token
- Storage - Token stored in cookie (automatic) or localStorage/header (manual)
- Requests - Token sent with each request for authentication
- Validation - Server validates signature and expiration
- Renewal - Cookie can auto-renew; header tokens require manual refresh
Key Concept: No server-side session storage. Token itself is the session.
Bknd采用基于JWT的无状态会话:
- 登录 - 服务器创建包含用户数据的签名JWT,返回令牌
- 存储 - 令牌存储在Cookie(自动)或localStorage/请求头(手动)中
- 请求 - 每个请求都携带令牌用于认证
- 验证 - 服务器验证签名和过期时间
- 续期 - Cookie可自动续期;请求头令牌需要手动刷新
核心概念: 无服务端会话存储,令牌本身即为会话。
Session Configuration
会话配置
JWT Settings
JWT设置
typescript
import { defineConfig } from "bknd";
export default defineConfig({
auth: {
enabled: true,
jwt: {
secret: process.env.JWT_SECRET!, // Required for production
alg: "HS256", // Algorithm: HS256 | HS384 | HS512
expires: 604800, // 7 days in seconds
issuer: "my-app", // Token issuer claim
fields: ["id", "email", "role"], // User fields in token payload
},
},
});JWT options:
| Option | Type | Default | Description |
|---|---|---|---|
| string | | Signing secret (256-bit min for production) |
| string | | HMAC algorithm |
| number | - | Token lifetime in seconds |
| string | - | Issuer claim (iss) |
| string[] | | User fields encoded in token |
typescript
import { defineConfig } from "bknd";
export default defineConfig({
auth: {
enabled: true,
jwt: {
secret: process.env.JWT_SECRET!, // 生产环境必填
alg: "HS256", // 算法:HS256 | HS384 | HS512
expires: 604800, // 有效期7天,单位秒
issuer: "my-app", // 令牌发行方声明
fields: ["id", "email", "role"], // 令牌载荷中的用户字段
},
},
});JWT选项说明:
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| string | | 签名密钥(生产环境至少256位) |
| string | | HMAC算法 |
| number | - | 令牌有效期(秒) |
| string | - | 发行方声明(iss) |
| string[] | | 编码到令牌中的用户字段 |
Cookie Settings
Cookie设置
typescript
{
auth: {
cookie: {
secure: process.env.NODE_ENV === "production", // HTTPS only
httpOnly: true, // No JS access
sameSite: "lax", // CSRF protection
expires: 604800, // Match JWT expiry
renew: true, // Auto-extend on activity
path: "/", // Cookie scope
pathSuccess: "/dashboard", // Redirect after login
pathLoggedOut: "/login", // Redirect after logout
},
},
}Cookie options:
| Option | Type | Default | Description |
|---|---|---|---|
| boolean | | Require HTTPS |
| boolean | | Block JavaScript access |
| string | | |
| number | | Cookie lifetime (seconds) |
| boolean | | Auto-renew on requests |
| string | | Post-login redirect |
| string | | Post-logout redirect |
typescript
{
auth: {
cookie: {
secure: process.env.NODE_ENV === "production", // 仅HTTPS环境启用
httpOnly: true, // 禁止JavaScript访问
sameSite: "lax", // CSRF保护
expires: 604800, // 与JWT有效期匹配
renew: true, // 有活动时自动续期
path: "/", // Cookie作用域
pathSuccess: "/dashboard", // 登录后重定向路径
pathLoggedOut: "/login", // 登出后重定向路径
},
},
}Cookie选项说明:
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| boolean | | 要求HTTPS环境 |
| boolean | | 阻止JavaScript访问 |
| string | | 可选值: |
| number | | Cookie有效期(秒) |
| boolean | | 请求时自动续期 |
| string | | 登录后重定向路径 |
| string | | 登出后重定向路径 |
SDK Approach
SDK实现方式
Session Persistence with Storage
基于存储的会话持久化
typescript
import { Api } from "bknd";
// Persistent sessions (survives page refresh/browser restart)
const api = new Api({
host: "http://localhost:7654",
storage: localStorage, // Token persisted
});
// Session-only (cleared when tab closes)
const api = new Api({
host: "http://localhost:7654",
storage: sessionStorage, // Token cleared on tab close
});
// No persistence (token in memory only)
const api = new Api({
host: "http://localhost:7654",
// No storage = token lost on page refresh
});typescript
import { Api } from "bknd";
// 持久化会话(页面刷新/浏览器重启后仍保留)
const api = new Api({
host: "http://localhost:7654",
storage: localStorage, // 令牌持久化存储
});
// 仅会话级存储(标签页关闭后清除)
const api = new Api({
host: "http://localhost:7654",
storage: sessionStorage, // 标签页关闭后令牌清除
});
// 无持久化(仅内存中存储令牌)
const api = new Api({
host: "http://localhost:7654",
// 不设置storage,页面刷新后令牌丢失
});Check Session on App Start
应用启动时检查会话
typescript
async function initializeAuth() {
const api = new Api({
host: "http://localhost:7654",
storage: localStorage,
});
// Check if existing token is still valid
const { ok, data } = await api.auth.me();
if (ok && data?.user) {
console.log("Session valid:", data.user.email);
return { api, user: data.user };
}
console.log("No valid session");
return { api, user: null };
}
// On app mount
const { api, user } = await initializeAuth();typescript
async function initializeAuth() {
const api = new Api({
host: "http://localhost:7654",
storage: localStorage,
});
// 检查现有令牌是否仍有效
const { ok, data } = await api.auth.me();
if (ok && data?.user) {
console.log("会话有效:", data.user.email);
return { api, user: data.user };
}
console.log("无有效会话");
return { api, user: null };
}
// 应用挂载时调用
const { api, user } = await initializeAuth();Session State Management
会话状态管理
typescript
import { Api } from "bknd";
class SessionManager {
private api: Api;
private user: User | null = null;
private listeners: Set<(user: User | null) => void> = new Set();
constructor(host: string) {
this.api = new Api({ host, storage: localStorage });
}
// Initialize - call on app start
async init() {
const { ok, data } = await this.api.auth.me();
this.user = ok ? data?.user ?? null : null;
this.notifyListeners();
return this.user;
}
// Get current session
getUser() {
return this.user;
}
isAuthenticated() {
return this.user !== null;
}
// Login - creates new session
async login(email: string, password: string) {
const { ok, data, error } = await this.api.auth.login("password", {
email,
password,
});
if (!ok) throw new Error(error?.message || "Login failed");
this.user = data!.user;
this.notifyListeners();
return this.user;
}
// Logout - destroys session
async logout() {
await this.api.auth.logout();
this.user = null;
this.notifyListeners();
}
// Refresh session (re-validate token)
async refresh() {
const { ok, data } = await this.api.auth.me();
this.user = ok ? data?.user ?? null : null;
this.notifyListeners();
return this.user;
}
// Subscribe to session changes
subscribe(callback: (user: User | null) => void) {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
}
private notifyListeners() {
this.listeners.forEach((cb) => cb(this.user));
}
}
type User = { id: number; email: string; role?: string };
// Usage
const session = new SessionManager("http://localhost:7654");
await session.init();
session.subscribe((user) => {
console.log("Session changed:", user?.email || "logged out");
});typescript
import { Api } from "bknd";
class SessionManager {
private api: Api;
private user: User | null = null;
private listeners: Set<(user: User | null) => void> = new Set();
constructor(host: string) {
this.api = new Api({ host, storage: localStorage });
}
// 初始化 - 应用启动时调用
async init() {
const { ok, data } = await this.api.auth.me();
this.user = ok ? data?.user ?? null : null;
this.notifyListeners();
return this.user;
}
// 获取当前会话
getUser() {
return this.user;
}
isAuthenticated() {
return this.user !== null;
}
// 登录 - 创建新会话
async login(email: string, password: string) {
const { ok, data, error } = await this.api.auth.login("password", {
email,
password,
});
if (!ok) throw new Error(error?.message || "登录失败");
this.user = data!.user;
this.notifyListeners();
return this.user;
}
// 登出 - 销毁会话
async logout() {
await this.api.auth.logout();
this.user = null;
this.notifyListeners();
}
// 刷新会话(重新验证令牌)
async refresh() {
const { ok, data } = await this.api.auth.me();
this.user = ok ? data?.user ?? null : null;
this.notifyListeners();
return this.user;
}
// 订阅会话变更
subscribe(callback: (user: User | null) => void) {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
}
private notifyListeners() {
this.listeners.forEach((cb) => cb(this.user));
}
}
type User = { id: number; email: string; role?: string };
// 使用示例
const session = new SessionManager("http://localhost:7654");
await session.init();
session.subscribe((user) => {
console.log("会话变更:", user?.email || "已登出");
});Cookie-Based Sessions (Automatic)
基于Cookie的会话(自动)
typescript
const api = new Api({
host: "http://localhost:7654",
tokenTransport: "cookie", // Use httpOnly cookies
});
// Login sets cookie automatically
await api.auth.login("password", { email, password });
// All requests include cookie automatically
await api.data.readMany("posts");
// Logout clears cookie
await api.auth.logout();Cookie mode advantages:
- HttpOnly = XSS protection (JavaScript can't access token)
- Auto-renewal on every request (if )
cookie.renew: true - No manual token management
- Automatic CSRF protection with
sameSite
typescript
const api = new Api({
host: "http://localhost:7654",
tokenTransport: "cookie", // 使用HttpOnly Cookie
});
// 登录时自动设置Cookie
await api.auth.login("password", { email, password });
// 所有请求自动携带Cookie
await api.data.readMany("posts");
// 登出时自动清除Cookie
await api.auth.logout();Cookie模式优势:
- HttpOnly = XSS防护(JavaScript无法访问令牌)
- 每次请求自动续期(如果)
cookie.renew: true - 无需手动管理令牌
- 结合自动实现CSRF防护
sameSite
Header-Based Sessions (Manual)
基于请求头的会话(手动)
typescript
const api = new Api({
host: "http://localhost:7654",
storage: localStorage,
tokenTransport: "header", // Default
});
// Token stored in localStorage, sent via Authorization header
await api.auth.login("password", { email, password });
// Token automatically included:
// Authorization: Bearer eyJhbGciOiJIUzI1NiIs...typescript
const api = new Api({
host: "http://localhost:7654",
storage: localStorage,
tokenTransport: "header", // 默认值
});
// 令牌存储在localStorage中,通过Authorization头发送
await api.auth.login("password", { email, password });
// 令牌自动包含在请求中:
// Authorization: Bearer eyJhbGciOiJIUzI1NiIs...Handling Session Expiration
处理会话过期
Detect Expired Token
检测过期令牌
typescript
async function makeAuthenticatedRequest<T>(fn: () => Promise<T>): Promise<T> {
try {
return await fn();
} catch (error) {
// Check if error is due to expired session
if (isAuthError(error)) {
// Session expired - redirect to login or refresh
await handleExpiredSession();
}
throw error;
}
}
function isAuthError(error: unknown): boolean {
if (error instanceof Error) {
return error.message.includes("401") || error.message.includes("Unauthorized");
}
return false;
}
async function handleExpiredSession() {
// Option 1: Redirect to login
window.location.href = "/login?expired=true";
// Option 2: Show re-authentication modal
// showReauthModal();
// Option 3: Try to refresh (if using refresh tokens)
// await refreshToken();
}typescript
async function makeAuthenticatedRequest<T>(fn: () => Promise<T>): Promise<T> {
try {
return await fn();
} catch (error) {
// 检查错误是否由会话过期导致
if (isAuthError(error)) {
// 会话过期 - 重定向到登录页或刷新会话
await handleExpiredSession();
}
throw error;
}
}
function isAuthError(error: unknown): boolean {
if (error instanceof Error) {
return error.message.includes("401") || error.message.includes("Unauthorized");
}
return false;
}
async function handleExpiredSession() {
// 选项1:重定向到登录页
window.location.href = "/login?expired=true";
// 选项2:显示重新认证弹窗
// showReauthModal();
// 选项3:尝试刷新令牌(如果使用刷新令牌)
// await refreshToken();
}Auto-Refresh Pattern
自动刷新模式
Since Bknd uses stateless JWT, there's no built-in refresh token. Instead, use to re-validate and extend cookie-based sessions:
api.auth.me()typescript
class SessionWithAutoRefresh {
private api: Api;
private refreshInterval: number | null = null;
constructor(host: string) {
this.api = new Api({
host,
tokenTransport: "cookie", // Cookie auto-renews on requests
});
}
// Start periodic session check
startAutoRefresh(intervalMs = 5 * 60 * 1000) {
// Every 5 minutes
this.refreshInterval = window.setInterval(async () => {
const { ok } = await this.api.auth.me();
if (!ok) {
this.stopAutoRefresh();
this.onSessionExpired();
}
}, intervalMs);
}
stopAutoRefresh() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
}
private onSessionExpired() {
// Handle expired session
window.location.href = "/login?session=expired";
}
}由于Bknd使用无状态JWT,没有内置的刷新令牌。可以使用重新验证并续期基于Cookie的会话:
api.auth.me()typescript
class SessionWithAutoRefresh {
private api: Api;
private refreshInterval: number | null = null;
constructor(host: string) {
this.api = new Api({
host,
tokenTransport: "cookie", // Cookie会在请求时自动续期
});
}
// 启动定期会话检查
startAutoRefresh(intervalMs = 5 * 60 * 1000) {
// 每5分钟检查一次
this.refreshInterval = window.setInterval(async () => {
const { ok } = await this.api.auth.me();
if (!ok) {
this.stopAutoRefresh();
this.onSessionExpired();
}
}, intervalMs);
}
stopAutoRefresh() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
}
private onSessionExpired() {
// 处理会话过期
window.location.href = "/login?session=expired";
}
}Proactive Token Refresh
主动令牌刷新
For header-based auth, re-login before token expires:
typescript
import { jwtDecode } from "jwt-decode"; // npm install jwt-decode
class TokenManager {
private api: Api;
private refreshTimer: number | null = null;
constructor(host: string) {
this.api = new Api({ host, storage: localStorage });
}
// Schedule refresh before expiry
scheduleRefresh(token: string) {
const decoded = jwtDecode<{ exp: number }>(token);
const expiresAt = decoded.exp * 1000; // Convert to ms
const refreshAt = expiresAt - 5 * 60 * 1000; // 5 min before expiry
const delay = refreshAt - Date.now();
if (delay > 0) {
this.refreshTimer = window.setTimeout(() => {
this.promptRelogin();
}, delay);
}
}
private promptRelogin() {
// Show modal asking user to re-authenticate
// Or redirect to login with return URL
}
cleanup() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
}
}
}对于基于请求头的认证,在令牌过期前重新登录:
typescript
import { jwtDecode } from "jwt-decode"; // npm install jwt-decode
class TokenManager {
private api: Api;
private refreshTimer: number | null = null;
constructor(host: string) {
this.api = new Api({ host, storage: localStorage });
}
// 在过期前安排刷新
scheduleRefresh(token: string) {
const decoded = jwtDecode<{ exp: number }>(token);
const expiresAt = decoded.exp * 1000; // 转换为毫秒
const refreshAt = expiresAt - 5 * 60 * 1000; // 过期前5分钟刷新
const delay = refreshAt - Date.now();
if (delay > 0) {
this.refreshTimer = window.setTimeout(() => {
this.promptRelogin();
}, delay);
}
}
private promptRelogin() {
// 显示弹窗要求用户重新认证
// 或重定向到登录页并携带返回URL
}
cleanup() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
}
}
}React Integration
React集成
Session Provider
会话提供者
tsx
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
import { Api } from "bknd";
type User = { id: number; email: string; role?: string };
type SessionContextType = {
user: User | null;
isLoading: boolean;
isAuthenticated: boolean;
checkSession: () => Promise<User | null>;
clearSession: () => void;
};
const SessionContext = createContext<SessionContextType | null>(null);
const api = new Api({
host: "http://localhost:7654",
storage: localStorage,
});
export function SessionProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Check session on mount
useEffect(() => {
checkSession().finally(() => setIsLoading(false));
}, []);
async function checkSession() {
const { ok, data } = await api.auth.me();
const user = ok ? data?.user ?? null : null;
setUser(user);
return user;
}
function clearSession() {
setUser(null);
api.auth.logout();
}
return (
<SessionContext.Provider
value={{
user,
isLoading,
isAuthenticated: user !== null,
checkSession,
clearSession,
}}
>
{children}
</SessionContext.Provider>
);
}
export function useSession() {
const context = useContext(SessionContext);
if (!context) throw new Error("useSession must be used within SessionProvider");
return context;
}tsx
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
import { Api } from "bknd";
type User = { id: number; email: string; role?: string };
type SessionContextType = {
user: User | null;
isLoading: boolean;
isAuthenticated: boolean;
checkSession: () => Promise<User | null>;
clearSession: () => void;
};
const SessionContext = createContext<SessionContextType | null>(null);
const api = new Api({
host: "http://localhost:7654",
storage: localStorage,
});
export function SessionProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// 挂载时检查会话
useEffect(() => {
checkSession().finally(() => setIsLoading(false));
}, []);
async function checkSession() {
const { ok, data } = await api.auth.me();
const user = ok ? data?.user ?? null : null;
setUser(user);
return user;
}
function clearSession() {
setUser(null);
api.auth.logout();
}
return (
<SessionContext.Provider
value={{
user,
isLoading,
isAuthenticated: user !== null,
checkSession,
clearSession,
}}
>
{children}
</SessionContext.Provider>
);
}
export function useSession() {
const context = useContext(SessionContext);
if (!context) throw new Error("useSession必须在SessionProvider内部使用");
return context;
}Session-Aware Components
会话感知组件
tsx
import { useSession } from "./SessionProvider";
function Header() {
const { user, isAuthenticated, clearSession } = useSession();
if (!isAuthenticated) {
return <a href="/login">Login</a>;
}
return (
<div>
<span>Welcome, {user!.email}</span>
<button onClick={clearSession}>Logout</button>
</div>
);
}
function ProtectedPage() {
const { isLoading, isAuthenticated } = useSession();
if (isLoading) return <div>Checking session...</div>;
if (!isAuthenticated) return <Navigate to="/login" />;
return <div>Protected content</div>;
}tsx
import { useSession } from "./SessionProvider";
function Header() {
const { user, isAuthenticated, clearSession } = useSession();
if (!isAuthenticated) {
return <a href="/login">登录</a>;
}
return (
<div>
<span>欢迎,{user!.email}</span>
<button onClick={clearSession}>登出</button>
</div>
);
}
function ProtectedPage() {
const { isLoading, isAuthenticated } = useSession();
if (isLoading) return <div>检查会话中...</div>;
if (!isAuthenticated) return <Navigate to="/login" />;
return <div>受保护内容</div>;
}Session Expiration Handler
会话过期处理器
tsx
import { useEffect } from "react";
import { useSession } from "./SessionProvider";
function SessionExpirationHandler() {
const { checkSession, clearSession } = useSession();
useEffect(() => {
// Check session periodically
const interval = setInterval(async () => {
const user = await checkSession();
if (!user) {
// Session expired
alert("Your session has expired. Please log in again.");
clearSession();
window.location.href = "/login";
}
}, 5 * 60 * 1000); // Every 5 minutes
// Check on window focus (user returns to tab)
const handleFocus = () => checkSession();
window.addEventListener("focus", handleFocus);
return () => {
clearInterval(interval);
window.removeEventListener("focus", handleFocus);
};
}, [checkSession, clearSession]);
return null; // Invisible component
}
// Add to app root
function App() {
return (
<SessionProvider>
<SessionExpirationHandler />
<Routes />
</SessionProvider>
);
}tsx
import { useEffect } from "react";
import { useSession } from "./SessionProvider";
function SessionExpirationHandler() {
const { checkSession, clearSession } = useSession();
useEffect(() => {
// 定期检查会话
const interval = setInterval(async () => {
const user = await checkSession();
if (!user) {
// 会话已过期
alert("您的会话已过期,请重新登录。");
clearSession();
window.location.href = "/login";
}
}, 5 * 60 * 1000); // 每5分钟检查一次
// 窗口获得焦点时检查(用户返回标签页)
const handleFocus = () => checkSession();
window.addEventListener("focus", handleFocus);
return () => {
clearInterval(interval);
window.removeEventListener("focus", handleFocus);
};
}, [checkSession, clearSession]);
return null; // 不可见组件
}
// 添加到应用根节点
function App() {
return (
<SessionProvider>
<SessionExpirationHandler />
<Routes />
</SessionProvider>
);
}Server-Side Session Validation
服务端会话验证
Validate Session in API Routes
API路由中的会话验证
typescript
import { getApi } from "bknd";
export async function GET(request: Request, app: BkndApp) {
const api = getApi(app);
const user = await api.auth.resolveAuthFromRequest(request);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
// Session valid - user data available
console.log("User ID:", user.id);
console.log("Email:", user.email);
console.log("Role:", user.role);
return new Response(JSON.stringify({ user }));
}typescript
import { getApi } from "bknd";
export async function GET(request: Request, app: BkndApp) {
const api = getApi(app);
const user = await api.auth.resolveAuthFromRequest(request);
if (!user) {
return new Response("未授权", { status: 401 });
}
// 会话有效 - 用户数据可用
console.log("用户ID:", user.id);
console.log("邮箱:", user.email);
console.log("角色:", user.role);
return new Response(JSON.stringify({ user }));
}Server-Side Session Check (Next.js)
服务端会话检查(Next.js)
typescript
// app/api/me/route.ts
import { getApp, getApi } from "bknd/adapter/nextjs";
export async function GET(request: Request) {
const app = await getApp();
const api = getApi(app);
const user = await api.auth.resolveAuthFromRequest(request);
if (!user) {
return Response.json({ user: null }, { status: 401 });
}
return Response.json({ user });
}typescript
// app/api/me/route.ts
import { getApp, getApi } from "bknd/adapter/nextjs";
export async function GET(request: Request) {
const app = await getApp();
const api = getApi(app);
const user = await api.auth.resolveAuthFromRequest(request);
if (!user) {
return Response.json({ user: null }, { status: 401 });
}
return Response.json({ user });
}Common Patterns
常见模式
Remember Last Activity
记录最后活动时间
typescript
// Track user activity for session timeout warnings
let lastActivity = Date.now();
// Update on user interaction
document.addEventListener("click", () => (lastActivity = Date.now()));
document.addEventListener("keypress", () => (lastActivity = Date.now()));
// Check for inactivity
setInterval(() => {
const inactiveMinutes = (Date.now() - lastActivity) / 1000 / 60;
if (inactiveMinutes > 25) {
// Warn user session will expire soon
showSessionWarning();
}
if (inactiveMinutes > 30) {
// Force logout
api.auth.logout();
window.location.href = "/login?reason=inactive";
}
}, 60000); // Check every minutetypescript
// 跟踪用户活动以发出会话超时警告
let lastActivity = Date.now();
// 用户交互时更新
document.addEventListener("click", () => (lastActivity = Date.now()));
document.addEventListener("keypress", () => (lastActivity = Date.now()));
// 检查是否处于非活动状态
setInterval(() => {
const inactiveMinutes = (Date.now() - lastActivity) / 1000 / 60;
if (inactiveMinutes > 25) {
// 警告用户会话即将过期
showSessionWarning();
}
if (inactiveMinutes > 30) {
// 强制登出
api.auth.logout();
window.location.href = "/login?reason=inactive";
}
}, 60000); // 每分钟检查一次Multi-Tab Session Sync
多标签页会话同步
typescript
// Sync session state across browser tabs
window.addEventListener("storage", async (event) => {
if (event.key === "auth") {
if (event.newValue === null) {
// Logged out in another tab
window.location.href = "/login";
} else {
// Logged in in another tab - refresh session
await api.auth.me();
window.location.reload();
}
}
});typescript
// 同步浏览器标签页间的会话状态
window.addEventListener("storage", async (event) => {
if (event.key === "auth") {
if (event.newValue === null) {
// 在其他标签页已登出
window.location.href = "/login";
} else {
// 在其他标签页已登录 - 刷新会话
await api.auth.me();
window.location.reload();
}
}
});Secure Session Storage
安全会话存储
typescript
// For sensitive apps, use sessionStorage + warn on tab close
const api = new Api({
host: "http://localhost:7654",
storage: sessionStorage,
});
window.addEventListener("beforeunload", (e) => {
if (api.auth.me()) {
e.preventDefault();
e.returnValue = "You will be logged out if you leave.";
}
});typescript
// 对于敏感应用,使用sessionStorage并在标签页关闭时发出警告
const api = new Api({
host: "http://localhost:7654",
storage: sessionStorage,
});
window.addEventListener("beforeunload", (e) => {
if (api.auth.me()) {
e.preventDefault();
e.returnValue = "离开此页面将导致您登出。";
}
});Common Pitfalls
常见陷阱
Session Lost on Refresh
页面刷新后会话丢失
Problem: User logged out after page refresh
Fix: Provide storage adapter:
typescript
// Wrong - no persistence
const api = new Api({ host: "http://localhost:7654" });
// Correct
const api = new Api({
host: "http://localhost:7654",
storage: localStorage,
});问题: 用户页面刷新后被登出
修复: 提供存储适配器:
typescript
// 错误示例 - 无持久化
const api = new Api({ host: "http://localhost:7654" });
// 正确示例
const api = new Api({
host: "http://localhost:7654",
storage: localStorage,
});Cookie Not Working Locally
Cookie在本地环境不工作
Problem: Cookie not set in development
Fix: Disable secure flag for localhost:
typescript
{
auth: {
cookie: {
secure: process.env.NODE_ENV === "production", // false in dev
},
},
}问题: 开发环境中Cookie未设置
修复: 本地环境禁用secure标志:
typescript
{
auth: {
cookie: {
secure: process.env.NODE_ENV === "production", // 开发环境为false
},
},
}Session Check Blocking UI
会话检查阻塞UI
Problem: App shows blank while checking session
Fix: Show loading state:
tsx
function App() {
const { isLoading } = useSession();
if (isLoading) {
return <LoadingSpinner />; // Don't leave blank
}
return <Routes />;
}问题: 应用在检查会话时显示空白
修复: 显示加载状态:
tsx
function App() {
const { isLoading } = useSession();
if (isLoading) {
return <LoadingSpinner />; // 不要留空白
}
return <Routes />;
}Expired Token Still in Storage
过期令牌仍存在于存储中
Problem: Old token causes continuous 401 errors
Fix: Clear storage on auth failure:
typescript
async function checkSession() {
const { ok } = await api.auth.me();
if (!ok) {
// Clear stale token
localStorage.removeItem("auth");
return null;
}
return user;
}问题: 旧令牌导致持续的401错误
修复: 认证失败时清除存储:
typescript
async function checkSession() {
const { ok } = await api.auth.me();
if (!ok) {
// 清除过期令牌
localStorage.removeItem("auth");
return null;
}
return user;
}Verification
验证
Test session handling:
1. Session persists across refresh:
typescript
// Login
await api.auth.login("password", { email: "test@example.com", password: "pass" });
// Refresh page, then:
const { ok, data } = await api.auth.me();
console.log("Session persists:", ok && data?.user); // Should be true2. Session expires correctly:
typescript
// Set short expiry in config (for testing)
jwt: { expires: 10 } // 10 seconds
// Login, wait 15 seconds
await api.auth.login("password", { email, password });
await new Promise(r => setTimeout(r, 15000));
const { ok } = await api.auth.me();
console.log("Session expired:", !ok); // Should be true3. Logout clears session:
typescript
await api.auth.logout();
const { ok } = await api.auth.me();
console.log("Session cleared:", !ok); // Should be true测试会话管理:
1. 会话在刷新后保持有效:
typescript
// 登录
await api.auth.login("password", { email: "test@example.com", password: "pass" });
// 刷新页面后执行:
const { ok, data } = await api.auth.me();
console.log("会话保持有效:", ok && data?.user); // 应为true2. 会话正确过期:
typescript
// 配置中设置短有效期(用于测试)
jwt: { expires: 10 } // 10秒
// 登录,等待15秒
await api.auth.login("password", { email, password });
await new Promise(r => setTimeout(r, 15000));
const { ok } = await api.auth.me();
console.log("会话已过期:", !ok); // 应为true3. 登出后会话被清除:
typescript
await api.auth.logout();
const { ok } = await api.auth.me();
console.log("会话已清除:", !ok); // 应为trueDOs and DON'Ts
注意事项
DO:
- Configure appropriate JWT expiry for your use case
- Use httpOnly cookies when possible (XSS protection)
- Check session validity on app initialization
- Handle session expiration gracefully with UI feedback
- Match cookie expiry with JWT expiry
- Use in production
secure: true
DON'T:
- Store tokens in memory only (lost on refresh)
- Use long expiry times without renewal mechanism
- Ignore session expiration errors
- Mix cookie and header auth without clear reason
- Disable httpOnly unless absolutely necessary
- Forget to clear storage on logout
建议:
- 根据业务场景配置合适的JWT有效期
- 尽可能使用HttpOnly Cookie(XSS防护)
- 应用初始化时检查会话有效性
- 优雅处理会话过期并提供UI反馈
- 保持Cookie有效期与JWT有效期一致
- 生产环境启用
secure: true
禁止:
- 仅在内存中存储令牌(刷新后丢失)
- 使用长有效期且无续期机制
- 忽略会话过期错误
- 无明确原因混合使用Cookie和请求头认证
- 除非必要否则禁用httpOnly
- 登出时忘记清除存储
Related Skills
相关技能
- bknd-setup-auth - Configure authentication system
- bknd-login-flow - Login/logout functionality
- bknd-oauth-setup - OAuth/social login providers
- bknd-protect-endpoint - Secure specific endpoints
- bknd-public-vs-auth - Configure public vs authenticated access
- bknd-setup-auth - 配置认证系统
- bknd-login-flow - 登录/登出功能
- bknd-oauth-setup - OAuth/社交登录提供商
- bknd-protect-endpoint - 保护特定端点
- bknd-public-vs-auth - 配置公开与认证访问权限