bknd-session-handling

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Session 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:
    bknd
    package installed
  • For React:
    @bknd/react
    package installed
  • 已启用认证功能的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:
  1. Login - Server creates signed JWT with user data, returns token
  2. Storage - Token stored in cookie (automatic) or localStorage/header (manual)
  3. Requests - Token sent with each request for authentication
  4. Validation - Server validates signature and expiration
  5. Renewal - Cookie can auto-renew; header tokens require manual refresh
Key Concept: No server-side session storage. Token itself is the session.
Bknd采用基于JWT的无状态会话
  1. 登录 - 服务器创建包含用户数据的签名JWT,返回令牌
  2. 存储 - 令牌存储在Cookie(自动)或localStorage/请求头(手动)中
  3. 请求 - 每个请求都携带令牌用于认证
  4. 验证 - 服务器验证签名和过期时间
  5. 续期 - 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:
OptionTypeDefaultDescription
secret
string
""
Signing secret (256-bit min for production)
alg
string
"HS256"
HMAC algorithm
expires
number-Token lifetime in seconds
issuer
string-Issuer claim (iss)
fields
string[]
["id","email","role"]
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选项说明:
选项类型默认值描述
secret
string
""
签名密钥(生产环境至少256位)
alg
string
"HS256"
HMAC算法
expires
number-令牌有效期(秒)
issuer
string-发行方声明(iss)
fields
string[]
["id","email","role"]
编码到令牌中的用户字段

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:
OptionTypeDefaultDescription
secure
boolean
true
Require HTTPS
httpOnly
boolean
true
Block JavaScript access
sameSite
string
"lax"
"strict"
|
"lax"
|
"none"
expires
number
604800
Cookie lifetime (seconds)
renew
boolean
true
Auto-renew on requests
pathSuccess
string
"/"
Post-login redirect
pathLoggedOut
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选项说明:
选项类型默认值描述
secure
boolean
true
要求HTTPS环境
httpOnly
boolean
true
阻止JavaScript访问
sameSite
string
"lax"
可选值:
"strict"
|
"lax"
|
"none"
expires
number
604800
Cookie有效期(秒)
renew
boolean
true
请求时自动续期
pathSuccess
string
"/"
登录后重定向路径
pathLoggedOut
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
  • 无需手动管理令牌
  • 结合
    sameSite
    自动实现CSRF防护

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
api.auth.me()
to re-validate and extend cookie-based sessions:
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,没有内置的刷新令牌。可以使用
api.auth.me()
重新验证并续期基于Cookie的会话:
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 minute
typescript
// 跟踪用户活动以发出会话超时警告
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 true
2. 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 true
3. 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);  // 应为true
2. 会话正确过期:
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);  // 应为true
3. 登出后会话被清除:
typescript
await api.auth.logout();
const { ok } = await api.auth.me();
console.log("会话已清除:", !ok);  // 应为true

DOs 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
    secure: true
    in production
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 - 配置公开与认证访问权限