发布于 2026-02-17
Lucia 提供基于 Session + Cookie 的登录校验方案,服务端通过校验会话拿到用户信息,前端通过受保护页面与接口的组合完成登录态管理。本篇笔记用于记录博客的 SSR 改造中,Lucia 在 Next.js App Router 中的实践。
lucia.validateSession 获取 session + user。lucia.invalidateSession 失效会话,并写入空 Cookie。getUserAttributes 把 auth_user.user_id 暴露为 linkedUserId,用于业务 user 查询。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.idexport 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 } },
});
}
服务层:
requireUser()validateRequest 已处理 Cookie 续期/清理,避免在路由层重复写 CookieAPI Route:
requireUser;API Route 捕获并返回 401前端:
AuthGate 统一保护。/api/auth/me