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 - 保护特定端点