/** * Next.js Middleware * 1. 前端路由保护(未登录重定向) * 2. API 路由权限控制(RBAC + API Token) * * 使用 Node.js Runtime 以支持 bcrypt/ioredis/crypto */ export const runtime = "nodejs"; import { NextRequest, NextResponse } from "next/server"; import { createHash } from "crypto"; import { parseSessionCookie, loadAdminSession, canAccess } from "@/lib/auth"; import { query } from "@/lib/db"; const PUBLIC_PATHS = ["/login", "/api/auth/login", "/api/auth/oidc"]; const PROTECTED_PATHS = ["/dashboard", "/admin", "/platform"]; function getRequiredPermission(path: string, method: string): string | null { if (path.startsWith("/api/users")) { if (method === "GET") return "user:read"; if (method === "POST") return "user:create"; if (method === "PUT" || method === "PATCH") return "user:update"; if (method === "DELETE") return "user:delete"; } if (path.startsWith("/api/roles")) { if (method === "GET") return "role:read"; if (method === "POST") return "role:create"; if (method === "PUT" || method === "PATCH") return "role:update"; if (method === "DELETE") return "role:delete"; } if (path.startsWith("/api/permissions")) { if (method === "GET") return "permission:read"; if (method === "POST") return "permission:create"; if (method === "PUT" || method === "PATCH") return "permission:update"; if (method === "DELETE") return "permission:delete"; } if (path.startsWith("/api/logs")) { if (method === "GET") return "log:read"; } if (path.startsWith("/api/sessions")) { return "session:manage"; } if (path.startsWith("/api/platform")) { if (method === "GET") return "platform:read"; return "platform:manage"; } if (path.startsWith("/api/admin/projects")) { if (method === "GET") return "platform:read"; return "platform:manage"; } if (path.startsWith("/api/api-tokens")) { return null; // Token 管理需要 session,不允许 token 访问自己 } return null; } /** 从 Authorization: Bearer 验证 API Token */ async function verifyApiToken(req: NextRequest): Promise< { valid: true; tokenId: number; permissions: string[] } | { valid: false } > { const authHeader = req.headers.get("Authorization"); if (!authHeader?.startsWith("Bearer ")) return { valid: false }; const token = authHeader.slice("Bearer ".length); if (!token) return { valid: false }; try { const hash = createHash("sha256").update(token).digest("hex"); const result = await query<{ id: number; permissions: string[]; is_active: boolean; expires_at: Date | null; }>( `SELECT id, permissions, is_active, expires_at FROM admin_api_token WHERE token_hash = $1`, [hash] ); if (!result.rows.length) return { valid: false }; const row = result.rows[0]; if (!row.is_active) return { valid: false }; if (row.expires_at && new Date(row.expires_at) < new Date()) return { valid: false }; // 更新 last_used_at(fire-and-forget) query(`UPDATE admin_api_token SET last_used_at = NOW() WHERE id = $1`, [row.id]).catch(() => {}); return { valid: true, tokenId: row.id, permissions: row.permissions || [] }; } catch { return { valid: false }; } } export async function middleware(req: NextRequest) { const { pathname } = req.nextUrl; const method = req.method; // 静态资源跳过 if ( pathname.startsWith("/_next") || pathname.startsWith("/favicon") || pathname.endsWith(".ico") || pathname.endsWith(".png") || pathname.endsWith(".svg") ) { return NextResponse.next(); } // 公开路由放行 if (PUBLIC_PATHS.some((p) => pathname.startsWith(p))) { return NextResponse.next(); } // ========== API 路由权限控制 ========== if (pathname.startsWith("/api/")) { const headers = new Headers(req.headers); let authType: "session" | "token" = "session"; let permissions: string[] = []; // 优先尝试 API Token 认证 const tokenResult = await verifyApiToken(req); if (tokenResult.valid) { authType = "token"; permissions = tokenResult.permissions || []; headers.set("x-admin-auth-type", "token"); headers.set("x-admin-token-id", String(tokenResult.tokenId)); } else { // 回退到 Session 认证 const cookieHeader = req.headers.get("cookie"); const sessionId = parseSessionCookie(cookieHeader); if (!sessionId) { return NextResponse.json({ error: "未登录或未提供 API Token" }, { status: 401 }); } const session = await loadAdminSession(sessionId); if (!session) { return NextResponse.json({ error: "会话已过期" }, { status: 401 }); } headers.set("x-admin-user-id", String(session.userId)); headers.set("x-admin-username", session.username); headers.set("x-admin-permissions", session.permissions.join(",")); permissions = session.permissions; } const requiredPermission = getRequiredPermission(pathname, method); if (requiredPermission && !permissions.some((p) => p === "*" || p === requiredPermission)) { return NextResponse.json({ error: "权限不足" }, { status: 403 }); } return NextResponse.next({ request: { headers } }); } // ========== 前端页面保护(仅 Session) ========== if (PROTECTED_PATHS.some((p) => pathname.startsWith(p))) { const cookieHeader = req.headers.get("cookie"); const sessionCookie = cookieHeader ?.split(";") .find((c) => c.trim().startsWith("admin_session=")); if (!sessionCookie) { return NextResponse.redirect(new URL("/login", req.url)); } } return NextResponse.next(); } export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], };