/** * 认证模块 * 支持: * 1. 账号密码登录(环境变量 + 数据库) * 2. OpenID Connect (OIDC) * * Session 存储在 Redis,Cookie 传输 */ import { v4 as uuidv4 } from "uuid"; import bcrypt from "bcrypt"; import * as jose from "jose"; import { ADMIN_SESSION_COOKIE_NAME, ADMIN_SESSION_TTL, OIDC_ENABLED, OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI, ADMIN_SUPER_USERNAME, ADMIN_SUPER_PASSWORD, ADMIN_SUPER_PASSWORD_HASH, COOKIE_SECURE, COOKIE_SAME_SITE, } from "./env"; import { saveSession, loadSession, deleteSession, refreshSessionTtl } from "./redis"; import { getUserByUsername, getAdminSession, createUser, type AdminSession, } from "./rbac"; import type { AdminUser } from "./rbac"; const SALT_ROUNDS = 12; // ============ 密码验证 ============ export async function verifyPassword( plain: string, hash: string ): Promise { return bcrypt.compare(plain, hash); } export async function hashPassword(plain: string): Promise { return bcrypt.hash(plain, SALT_ROUNDS); } // ============ 环境变量超级管理员 ============ async function verifySuperAdmin( username: string, password: string ): Promise { if ( !ADMIN_SUPER_USERNAME || username !== ADMIN_SUPER_USERNAME ) { return null; } // 优先使用预哈希密码 if (ADMIN_SUPER_PASSWORD_HASH) { const valid = await bcrypt.compare(password, ADMIN_SUPER_PASSWORD_HASH); if (!valid) return null; } else if (ADMIN_SUPER_PASSWORD) { if (password !== ADMIN_SUPER_PASSWORD) return null; } else { return null; } // 返回虚超级管理员用户对象(ID=-1) return { id: -1, username: ADMIN_SUPER_USERNAME, password_hash: ADMIN_SUPER_PASSWORD_HASH, is_active: true, created_at: new Date(), updated_at: new Date(), } as AdminUser; } // ============ 登录 ============ export async function login( username: string, password: string ): Promise<{ sessionId: string; adminSession: AdminSession } | null> { // 1. 尝试超级管理员 const superUser = await verifySuperAdmin(username, password); let adminUser: AdminUser | null = superUser; let isSuperUser = !!superUser; // 2. 尝试数据库用户 if (!adminUser) { const dbUser = await getUserByUsername(username); if (!dbUser || !dbUser.is_active) return null; const valid = await verifyPassword(password, dbUser.password_hash); if (!valid) return null; adminUser = dbUser; isSuperUser = false; } // 3. 生成 session const sessionId = uuidv4(); const now = new Date().toISOString(); // 获取权限信息 let adminSession: AdminSession; if (isSuperUser) { adminSession = { userId: adminUser.id, username: adminUser.username, roles: ["super_admin"], permissions: ["*"], // 超级管理员拥有所有权限 }; } else { adminSession = await getAdminSession(adminUser.id, adminUser.username); } // 4. 保存到 Redis const sessionState: Record = { "session:user_uid": adminUser.id, "session:username": adminUser.username, "session:roles": adminSession.roles, "session:permissions": adminSession.permissions, "session:created_at": now, "session:last_active": now, "session:ip_address": null, "session:user_agent": null, }; await saveSession(sessionId, sessionState, ADMIN_SESSION_TTL); return { sessionId, adminSession }; } // ============ Session 加载 ============ export async function loadAdminSession( sessionId: string ): Promise { const state = await loadSession(sessionId); if (!state) return null; const userId = state["session:user_uid"]; const username = state["session:username"] as string; const roles = (state["session:roles"] as string[]) || []; const permissions = (state["session:permissions"] as string[]) || []; if (!userId || !username) return null; return { userId: userId as number, username, roles, permissions, }; } // ============ 刷新活跃时间 ============ export async function touchSession(sessionId: string): Promise { const state = await loadSession(sessionId); if (!state) return; state["session:last_active"] = new Date().toISOString(); const { saveSession } = await import("@/lib/redis"); await saveSession(sessionId, state, ADMIN_SESSION_TTL); } // ============ 登出 ============ export async function logout(sessionId: string): Promise { await deleteSession(sessionId); } // ============ OIDC ============ export function isOidcEnabled(): boolean { return ( OIDC_ENABLED && !!OIDC_ISSUER && !!OIDC_CLIENT_ID && !!OIDC_CLIENT_SECRET ); } export function buildOidcAuthUrl(): string { const params = new URLSearchParams({ client_id: OIDC_CLIENT_ID, redirect_uri: OIDC_REDIRECT_URI, response_type: "code", scope: "openid profile email", }); return `${OIDC_ISSUER}/authorize?${params.toString()}`; } export async function exchangeOidcCode( code: string ): Promise<{ sessionId: string; adminSession: AdminSession } | null> { try { // 1. 用 code 换 token const tokenRes = await fetch(`${OIDC_ISSUER}/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: OIDC_REDIRECT_URI, client_id: OIDC_CLIENT_ID, client_secret: OIDC_CLIENT_SECRET, }), }); if (!tokenRes.ok) return null; const tokenData = await tokenRes.json() as { access_token?: string; id_token?: string; }; const idToken = tokenData.id_token || tokenData.access_token; if (!idToken) return null; // 2. 验证并解码 id_token const JWKS = jose.createRemoteJWKSet( new URL(`${OIDC_ISSUER}/.well-known/jwks.json`) ); const { payload } = await jose.jwtVerify(idToken, JWKS, { issuer: OIDC_ISSUER, audience: OIDC_CLIENT_ID, }); const email = payload.email as string; const name = (payload.name || payload.preferred_username || email) as string; // 3. 查找或创建 admin 用户 let user = await getUserByUsername(email); if (!user) { // 自动创建(首次 OIDC 登录) const randomPassword = uuidv4(); const passwordHash = await hashPassword(randomPassword); user = await createUser(email, passwordHash); } if (!user.is_active) return null; // 4. 生成 session const sessionId = uuidv4(); const now = new Date().toISOString(); const adminSession = await getAdminSession(user.id, user.username); const sessionState: Record = { "session:user_uid": user.id, "session:username": user.username, "session:roles": adminSession.roles, "session:permissions": adminSession.permissions, "session:created_at": now, "session:last_active": now, "session:oidc_name": name, "session:ip_address": null, "session:user_agent": null, }; await saveSession(sessionId, sessionState, ADMIN_SESSION_TTL); return { sessionId, adminSession }; } catch { return null; } } // ============ Cookie 工具 ============ export interface CookieOptions { maxAge?: number; path?: string; domain?: string; sameSite?: "strict" | "lax" | "none"; httpOnly?: boolean; secure?: boolean; } export function buildSetCookieHeader( value: string, options: CookieOptions = {} ): string { const { maxAge = ADMIN_SESSION_TTL, path = "/", sameSite = COOKIE_SAME_SITE, httpOnly = true, secure = COOKIE_SECURE, } = options; let cookie = `${ADMIN_SESSION_COOKIE_NAME}=${encodeURIComponent(value)}; Path=${path}; Max-Age=${maxAge}; SameSite=${sameSite}`; if (secure) cookie += "; Secure"; if (httpOnly) cookie += "; HttpOnly"; return cookie; } export function parseSessionCookie(cookieHeader: string | null): string | null { if (!cookieHeader) return null; const cookies = cookieHeader.split(";").map((c) => c.trim()); for (const cookie of cookies) { const [name, ...valueParts] = cookie.split("="); if (name === ADMIN_SESSION_COOKIE_NAME) { return decodeURIComponent(valueParts.join("=")); } } return null; } export function buildClearCookieHeader(): string { return `${ADMIN_SESSION_COOKIE_NAME}=; Path=/; Max-Age=0; SameSite=${COOKIE_SAME_SITE}${COOKIE_SECURE ? "; Secure" : ""}; HttpOnly`; } // ============ 权限检查 ============ export function canAccess( session: AdminSession | null, requiredPermission: string ): boolean { if (!session) return false; if (session.permissions.includes("*")) return true; return session.permissions.includes(requiredPermission); } export function isSuperAdmin(session: AdminSession | null): boolean { return session?.roles.includes("super_admin") ?? false; } /** 从 NextRequest headers 提取当前登录的 admin user id(由 middleware 设置) */ export function getAdminUserId(req: Request): number | null { const header = req.headers.get("x-admin-user-id"); if (!header) return null; const id = parseInt(header, 10); return isNaN(id) ? null : id; }