Bknd登录注销流程Skill bknd-login-flow

此技能用于在Bknd应用中实现用户认证流程,包括登录、注销和会话检查功能,涵盖SDK认证方法、REST API端点、React集成、错误处理等。关键词:Bknd、登录、注销、认证、前端开发、React、REST API、SDK、错误处理、会话管理。

前端开发 0 次安装 0 次浏览 更新于 3/8/2026

name: bknd-login-flow description: 在Bknd应用中实现登录和注销功能时使用。覆盖SDK认证方法、REST API端点、React集成、会话检查和错误处理。

登录/注销流程

在Bknd中实现用户认证流程,包括登录、注销和会话检查。

先决条件

  • 启用认证的Bknd项目(bknd-setup-auth
  • 至少配置一种认证策略(密码、OAuth)
  • 对于SDK:已安装bknd
  • 对于React:已安装@bknd/react

何时使用UI模式

  • 通过管理面板测试登录/注销端点
  • 查看活动会话
  • 检查用户认证状态

UI步骤: 管理面板 > 认证 > 测试端点

何时使用代码模式

  • 在前端实现登录表单
  • 添加注销功能
  • 检查认证状态
  • 构建受保护路由
  • 处理认证错误

SDK方法

初始化API客户端

import { Api } from "bknd";

const api = new Api({
  host: "http://localhost:7654",
  storage: localStorage,  // 在会话间持久化令牌
});

SDK选项:

选项 类型 描述
host string 后端URL
storage Storage 令牌持久化(localStorage、sessionStorage)
token string 预设认证令牌
tokenTransport "header" | "cookie" 令牌发送方式(默认:header)

使用密码策略登录

async function login(email: string, password: string) {
  const { ok, data, error, status } = await api.auth.login("password", {
    email,
    password,
  });

  if (ok) {
    // 令牌自动存储在localStorage中
    console.log("Logged in as:", data.user.email);
    console.log("User ID:", data.user.id);
    console.log("Role:", data.user.role);
    return data.user;
  }

  // 处理错误
  if (status === 401) {
    throw new Error("Invalid email or password");
  }
  if (status === 403) {
    throw new Error("Account uses different login method");
  }
  throw new Error(error?.message || "Login failed");
}

登录响应:

type LoginResponse = {
  ok: boolean;
  status: number;
  data?: {
    user: {
      id: number | string;
      email: string;
      role?: string;
      // 自定义字段...
    };
    token: string;
  };
  error?: { message: string };
};

检查当前用户

async function getCurrentUser() {
  const { ok, data } = await api.auth.me();

  if (ok && data?.user) {
    return data.user;  // 用户已认证
  }
  return null;  // 未认证
}

注销

async function logout() {
  await api.auth.logout();
  // 令牌从存储中移除
  // 用户现已注销
}

检查是否已认证

async function isAuthenticated(): Promise<boolean> {
  const { ok, data } = await api.auth.me();
  return ok && data?.user !== null;
}

完整登录流程示例

import { Api } from "bknd";

class AuthService {
  private api: Api;

  constructor() {
    this.api = new Api({
      host: import.meta.env.VITE_API_URL || "http://localhost:7654",
      storage: localStorage,
    });
  }

  async login(email: string, password: string) {
    const result = await this.api.auth.login("password", { email, password });

    if (!result.ok) {
      throw new AuthError(result.status, result.error?.message);
    }

    return result.data!.user;
  }

  async logout() {
    await this.api.auth.logout();
  }

  async getUser() {
    const { ok, data } = await this.api.auth.me();
    return ok ? data?.user : null;
  }

  async isAuthenticated() {
    const user = await this.getUser();
    return user !== null;
  }
}

class AuthError extends Error {
  constructor(public status: number, message?: string) {
    super(message || "Authentication failed");
    this.name = "AuthError";
  }
}

// 使用
const auth = new AuthService();

try {
  const user = await auth.login("user@example.com", "password123");
  console.log("Welcome,", user.email);
} catch (e) {
  if (e instanceof AuthError && e.status === 401) {
    console.error("Wrong credentials");
  }
}

REST API方法

通过REST登录

# 登录
curl -X POST http://localhost:7654/api/auth/password/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "password123"}'

响应:

{
  "user": {
    "id": 1,
    "email": "user@example.com",
    "role": "user"
  },
  "token": "eyJhbGciOiJIUzI1NiIs..."
}

在请求中使用令牌

# 获取当前用户
curl http://localhost:7654/api/auth/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

# 访问受保护数据
curl http://localhost:7654/api/data/posts \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

通过REST注销

curl -X POST http://localhost:7654/api/auth/logout \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

注意: 注销清除服务器端cookie。对于基于header的认证,客户端必须丢弃令牌。

REST端点参考

方法 路径 描述
POST /api/auth/password/login 使用邮箱/密码登录
GET /api/auth/me 获取当前认证用户
POST /api/auth/logout 注销(清除会话)
GET /api/auth/strategies 列出可用策略

React集成

使用useAuth钩子

import { BkndProvider, useAuth } from "@bknd/react";

function App() {
  return (
    <BkndProvider config={{ host: "http://localhost:7654" }}>
      <AuthExample />
    </BkndProvider>
  );
}

function AuthExample() {
  const { user, isLoading, login, logout } = useAuth();

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (!user) {
    return <LoginForm onLogin={login} />;
  }

  return (
    <div>
      <p>Welcome, {user.email}!</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

登录表单组件

import { useState } from "react";
import { useAuth } from "@bknd/react";

function LoginForm() {
  const { login } = useAuth();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState<string | null>(null);
  const [isSubmitting, setIsSubmitting] = useState(false);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setError(null);
    setIsSubmitting(true);

    try {
      await login("password", { email, password });
      // 重定向或更新UI - 用户状态自动更新
    } catch (err) {
      setError(err instanceof Error ? err.message : "Login failed");
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {error && <div className="error">{error}</div>}

      <label>
        邮箱:
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
      </label>

      <label>
        密码:
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
      </label>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "登录中..." : "登录"}
      </button>
    </form>
  );
}

受保护路由模式

import { Navigate } from "react-router-dom";
import { useAuth } from "@bknd/react";

function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const { user, isLoading } = useAuth();

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (!user) {
    return <Navigate to="/login" replace />;
  }

  return <>{children}</>;
}

// 与React Router使用
function AppRoutes() {
  return (
    <Routes>
      <Route path="/login" element={<LoginPage />} />
      <Route
        path="/dashboard"
        element={
          <ProtectedRoute>
            <DashboardPage />
          </ProtectedRoute>
        }
      />
    </Routes>
  );
}

注销按钮组件

import { useAuth } from "@bknd/react";
import { useNavigate } from "react-router-dom";

function LogoutButton() {
  const { logout } = useAuth();
  const navigate = useNavigate();

  async function handleLogout() {
    await logout();
    navigate("/login");
  }

  return <button onClick={handleLogout}>Logout</button>;
}

手动API与React集成

如果不使用@bknd/react

import { useState, useEffect, createContext, useContext } from "react";
import { Api } from "bknd";

type User = { id: number; email: string; role?: string };

type AuthContextType = {
  user: User | null;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
};

const AuthContext = createContext<AuthContextType | null>(null);

const api = new Api({
  host: "http://localhost:7654",
  storage: localStorage,
});

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  // 挂载时检查认证
  useEffect(() => {
    api.auth.me().then(({ ok, data }) => {
      setUser(ok ? data?.user ?? null : null);
      setIsLoading(false);
    });
  }, []);

  async function login(email: string, password: string) {
    const { ok, data, error } = await api.auth.login("password", {
      email,
      password,
    });

    if (!ok) {
      throw new Error(error?.message || "Login failed");
    }

    setUser(data!.user);
  }

  async function logout() {
    await api.auth.logout();
    setUser(null);
  }

  return (
    <AuthContext.Provider value={{ user, isLoading, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) throw new Error("useAuth must be used within AuthProvider");
  return context;
}

基于Cookie的认证

对于使用cookie传输的浏览器应用:

const api = new Api({
  host: "http://localhost:7654",
  tokenTransport: "cookie",  // 使用cookie代替header
});

// 登录自动设置httpOnly cookie
await api.auth.login("password", { email, password });

// 后续请求自动包含cookie
await api.data.readMany("posts");

// 注销清除cookie
await api.auth.logout();

Cookie模式优势:

  • HttpOnly cookie无法通过JavaScript访问(XSS保护)
  • 请求时自动续订(如配置)
  • 无需手动管理令牌

错误处理

错误代码

状态码 含义 常见原因
400 错误请求 缺少邮箱/密码
401 未授权 无效凭据
403 禁止访问 错误策略、账户禁用
409 冲突 账户使用不同登录方法

处理特定错误

async function handleLogin(email: string, password: string) {
  const { ok, status, error } = await api.auth.login("password", {
    email,
    password,
  });

  if (ok) return;

  switch (status) {
    case 400:
      throw new Error("请输入邮箱和密码");
    case 401:
      throw new Error("无效邮箱或密码");
    case 403:
      throw new Error("此账户使用不同登录方法");
    case 409:
      throw new Error("请使用社交登录访问此账户");
    default:
      throw new Error(error?.message || "登录失败,请重试。");
  }
}

常见模式

记住我

在localStorage(持久)与sessionStorage(会话)中存储令牌:

// 记住我启用
const api = new Api({
  host: "http://localhost:7654",
  storage: localStorage,  // 跨浏览器会话持久化
});

// 记住我禁用
const api = new Api({
  host: "http://localhost:7654",
  storage: sessionStorage,  // 标签页关闭时清除
});

自动刷新令牌

如果使用短寿命令牌,在过期前刷新:

async function withAuth<T>(fn: () => Promise<T>): Promise<T> {
  try {
    return await fn();
  } catch (e) {
    // 如果是401,尝试刷新并重试
    if (e instanceof Error && e.message.includes("401")) {
      const { ok } = await api.auth.me();  // 刷新令牌
      if (ok) return await fn();
    }
    throw e;
  }
}

登录后重定向

// 存储预期目的地
const redirectUrl = new URLSearchParams(window.location.search).get("redirect");

async function login(email: string, password: string) {
  await api.auth.login("password", { email, password });

  // 重定向到原始目的地或默认
  window.location.href = redirectUrl || "/dashboard";
}

常见陷阱

令牌未持久化

问题: 页面刷新后用户注销

修复: 为API客户端提供存储选项:

// 错误 - 刷新后令牌丢失
const api = new Api({ host: "http://localhost:7654" });

// 正确 - 令牌持久化
const api = new Api({
  host: "http://localhost:7654",
  storage: localStorage,
});

CORS错误

问题: 登录失败并出现CORS错误

修复: 在后端配置CORS或使用同源:

// 后端配置
{
  server: {
    cors: {
      origin: ["http://localhost:3000"],
      credentials: true,
    },
  },
}

Cookie未设置(HTTPS)

问题: 浏览器未接收cookie

修复: 本地开发时禁用secure

// 后端配置
{
  auth: {
    cookie: {
      secure: process.env.NODE_ENV === "production",
    },
  },
}

多策略冲突

问题: 用户通过不同策略注册

解决方案: 用户只能有一种策略。如果通过OAuth注册,则不能使用密码登录。先检查用户策略或引导到正确登录方法。

验证

测试登录流程:

1. 登录并获取令牌:

curl -X POST http://localhost:7654/api/auth/password/login \
  -H "Content-Type: application/json" \
  -d '{"email": "test@example.com", "password": "password123"}'

2. 使用令牌访问受保护端点:

curl http://localhost:7654/api/auth/me \
  -H "Authorization: Bearer <token>"

3. 验证注销清除会话:

curl -X POST http://localhost:7654/api/auth/logout \
  -H "Authorization: Bearer <token>"

# 现在应失败
curl http://localhost:7654/api/auth/me \
  -H "Authorization: Bearer <token>"

该做与不该做

该做:

  • 安全存储令牌(localStorage或httpOnly cookie)
  • 处理登录流程中所有错误情况
  • 在认证操作期间显示加载状态
  • 成功登录后重定向用户
  • 注销时完全清除认证状态

不该做:

  • 仅在内存中存储令牌(刷新后丢失)
  • 忽略登录错误处理
  • 在URL中暴露令牌
  • 忘记处理过期令牌
  • 无理由混合cookie和header认证

相关技能

  • bknd-setup-auth - 配置认证系统
  • bknd-registration - 设置用户注册
  • bknd-session-handling - 管理用户会话
  • bknd-oauth-setup - OAuth/社交登录提供商
  • bknd-protect-endpoint - 保护特定端点