黑白梦黑白梦

toggle navtoggle nav
  • 文章
  • 专栏
  • 文章
  • 专栏

Lucia 登录校验与项目实践

发布于 2026-02-17

Lucia 提供基于 Session + Cookie 的登录校验方案,服务端通过校验会话拿到用户信息,前端通过受保护页面与接口的组合完成登录态管理。本篇笔记用于记录博客的 SSR 改造中,Lucia 在 Next.js App Router 中的实践。

Lucia 登录校验的核心流程

  1. 登录:校验账号密码,创建 Lucia Session,生成并写入 Session Cookie。
  2. 校验:从 Cookie 读 sessionId,lucia.validateSession 获取 session + user。
  3. 续期/清理:session fresh 时刷新 Cookie;session 无效时清空 Cookie。
  4. 退出:lucia.invalidateSession 失效会话,并写入空 Cookie。

项目实践

Lucia 初始化与用户属性映射

  • 使用 drizzle adapter 适配原有 ORM ,无需修改原业务逻辑
  • getUserAttributes 把 auth_user.user_id 暴露为 linkedUserId,用于业务 user 查询。
  • Session Cookie 配置 secure/sameSite/path。
import { DrizzleMySQLAdapter } from '@lucia-auth/adapter-drizzle';
import { Lucia } from 'lucia';
import { db } from '@/api/db/db';
import { authUserTable, sessionTable } from '@/api/db/schema';
import { sessionCookieName } from '@/utils/env-helper';

const adapter = new DrizzleMySQLAdapter(db, sessionTable, authUserTable);

export const lucia = new Lucia(adapter, {
  sessionCookie: {
    name: sessionCookieName,
    expires: false,
    attributes: {
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      path: '/',
    },
  },
  getUserAttributes: (attributes) => ({
    linkedUserId: attributes.user_id,
  }),
});

认证表结构与桥接关系

  • auth_user.id 是 Lucia 的 user 主键(字符串)
  • auth_user.user_id 映射业务 user.id(整型)
  • session.user_id 指向 auth_user.id
  • 这样保留了业务用户主键类型,同时兼容 Lucia 的会话模型。
export const authUserTable = mysqlTable('auth_user', {
  id: varchar('id', { length: 255 }).primaryKey(),
  user_id: int('user_id').notNull().references(() => userTable.id),
  create_time: datetime('create_time').notNull().$defaultFn(() => new Date()),
});

export const sessionTable = mysqlTable('session', {
  id: varchar('id', { length: 255 }).primaryKey(),
  userId: varchar('user_id', { length: 255 })
    .notNull()
    .references(() => authUserTable.id),
  expiresAt: datetime('expires_at').notNull(),
});

登录与退出

登录后,根据用户,查询认证表中的 id 信息,执行 createSession 。

export async function loginWithPassword(input: { username: string; password: string }) {
  const [user] = await getUserFromdb()
  const [authUser] = await db
    .select()
    .from(authUserTable)
    .where(eq(authUserTable.user_id, user.id))
    .limit(1);
  const authUserId = authUser?.id ?? generateIdFromEntropySize(10);
  if (!authUser) await db.insert(authUserTable).values({ id: authUserId, user_id: user.id });
  const session = await lucia.createSession(authUserId, {});
  return { user, session };
}

在登录接口中,把 Lucia 的 session 写到 Next.js 的 cookie 中。

import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';

export async function POST(request: Request) {
  const parsed = loginSchema.safeParse(await request.json());
  if (!parsed.success) {
    return NextResponse.json({ ok: false }, { status: 400 });
  }
  const result = await loginWithPassword(parsed.data);
  if (!result) {
    return NextResponse.json({ ok: false }, { status: 401 });
  }
  const sessionCookie = lucia.createSessionCookie(result.session.id);
  const cookieStore = await cookies();
  cookieStore.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
  return NextResponse.json({ ok: true, data: { user: result.user } });
}

登出接口,判断到用户已登录(后面会说),拿到 session ,调用 Locia 执行 createBlankSessionCookie 。

export async function POST() {
  const { session } = await validateRequest();
  if (!session) {
    return NextResponse.json({ ok: false }, { status: 401 });
  }
  await lucia.invalidateSession(session.id);
  const sessionCookie = lucia.createBlankSessionCookie();
  const cookieStore = await cookies();
  cookieStore.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
  return NextResponse.json({ ok: true });
}

服务端校验与用户读取

直接通过 cookie 判断当前 session 是否仍然有效,如果有效则刷新 cookie 有效期,无效则执行 createBlankSessionCookie 。

export const validateRequest = cache(async () => {
  const cookieStore = await cookies();
  const sessionId = cookieStore.get(lucia.sessionCookieName)?.value ?? null;
  if (!sessionId) return { user: null, session: null };
  const result = await lucia.validateSession(sessionId);
  if (result.session?.fresh) {
    const sessionCookie = lucia.createSessionCookie(result.session.id);
    cookieStore.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
  }
  if (!result.session) {
    const sessionCookie = lucia.createBlankSessionCookie();
    cookieStore.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
  }
  return result;
});

对于需要获取当前登录用户详细信息的,从 lucia 拿到用户 id ,进行查表。

export async function getCurrentUser() {
  const { user } = await validateRequest();
  if (!user) return null;
  const [dbUser] = await db
    .select()
    .from(userTable)
    .where(eq(userTable.id, user.linkedUserId))
    .limit(1);
  if (!dbUser) return null;
  return { id: dbUser.id, username: dbUser.username, nickname: dbUser.nickname };
}

export async function requireUser() {
  const user = await getCurrentUser();
  if (!user) throw new Error('UNAUTHORIZED');
  return user;
}

前端登录态与页面保护

进入前端时获取一次用户信息,拿不到则跳转登录页。

export default function AuthGate({ children }: { children: ReactNode }) {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const redirectTo = pathname + (searchParams.toString() ? `?${searchParams.toString()}` : '');
  const me = useQuery({ queryKey: ['me'], queryFn: fetchMe });

  useEffect(() => {
    const user = me.data?.data?.user ?? null;
    if (me.isFetched && !user) {
      router.replace(`/admin/login?redirect=${encodeURIComponent(redirectTo)}`);
      router.refresh();
    }
  }, [me.isFetched, me.data, router, redirectTo]);

  if (!me.isFetched) return null;
  if (!me.data?.data?.user) return null;
  return children;
}

对应的接口实现,只需要获取当前用户并返回必要信息。

export async function GET() {
  const user = await getCurrentUser();
  if (!user) return NextResponse.json({ ok: true, data: { user: null } });
  return NextResponse.json({
    ok: true,
    data: { user: { id: user.id, username: user.username, nickname: user.nickname } },
  });
}

使用方式总结

服务层:

  • 需要权限的 service 统一调用 requireUser()
  • API Route / Server Action 只做参数校验与调用 service
  • validateRequest 已处理 Cookie 续期/清理,避免在路由层重复写 Cookie

API Route:

  • 登录:验证参数 + 调 service 创建 session + 写入 Cookie
  • 需要校验的接口:在 service 内用 requireUser;API Route 捕获并返回 401

前端:

  • 管理端页面使用 AuthGate 统一保护。
  • 页面内需要用户信息时调用 /api/auth/me
  • 前端只做体验层拦截,权限判断仍在服务层执行

©2015-2026 黑白梦 粤ICP备15018165号

联系: heibaimeng@foxmail.com