为什么需要迁移
Lucia
的作者计划明年 2025 年 1 月 1 日 停止 Lucia 3
的维护。
他觉得 Lucia
应该成为 JavaScript 实现身份验证的开源资源。
因为它底层的实现非常简单,就基于两个库:@oslojs/encoding
和 @oslojs/crypto
。
所以作者希望我们将学习如何从头开始实现权限控制,而不是安装该库。
具体可以参考:
https://github.com/lucia-auth/lucia/discussions/1707
即使这样我为什么还要继续使用 Lucia?
之前尝试过使用 next-auth
,当然文档还是个最大问题,在 coding
的过程中我还需要去查看它的源码才能满足自己的需求,以及库作者对于 password
的看法,最终还是决定使用 Lucia。
以下是我使用 Lucia 的理由:
- 因为 Lucia 仍然是一个很好的从头开始实现身份验证的原始库。
- 独立于付费的第三方
- 可以学习身份验证,掌握控制权
安装依赖
pnpm add @oslojs/encoding @oslojs/crypto
如果之前有安装过 lucia 和相关的 adapter,请先删除,我删除的是:
pnpm remove lucia @lucia-auth/adapter-drizzle
lib/lucia/crypto.ts
首先创建 lib/lucia/crypto.ts
文件,该文件主要是生成 session token 和 hash token 的逻辑。
import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
export function generateIdFromEntropySize(size: number): string {
const buffer = crypto.getRandomValues(new Uint8Array(size));
return encodeBase32LowerCaseNoPadding(buffer);
}
export function hashToken(token: string) {
return encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
}
lib/lucia/index.ts
创建 lib/lucia/index.ts
文件,该文件主要是创建 session 和验证 session token 的逻辑。
我这边用的是 drizzle-orm
,所以在 session
和 user
需要引入 drizzle-orm
的类型。
如果你用的是 prisma
或者 typeorm
等其他数据库,请自行替换。
基础类型
这部分主要是定义 session 验证的结果类型。
定义了 session 的过期时间 30 天,你可以根据实际情况调整。
并且定义了 session 如果到了 15 天,则需要刷新 session。
import { Session } from '@/zod/session';
import { SelectUser } from '@/zod/user';
export type SessionValidationResult =
| { session: Session; user: SelectUser; fresh: boolean }
| { session: null; user: null; fresh: boolean };
const SESSION_REFRESH_INTERVAL_MS = 1000 * 60 * 60 * 24 * 15; // 15 days
const SESSION_MAX_DURATION_MS = SESSION_REFRESH_INTERVAL_MS * 2; // 30 days
生成 session token
import { generateIdFromEntropySize, hashToken } from './crypto';
export function generateSessionToken(): string {
const token = generateIdFromEntropySize(20);
return hashToken(token);
}
创建 session
我这边的 db
是 drizzle-orm
,所以创建 session 的逻辑如下:
import { db } from '@/db';
import { sessionTable } from '@/db/schema';
export async function createSession(sessionToken: string, userId: string): Promise<Session> {
const session = {
id: sessionToken,
userId,
expiresAt: new Date(Date.now() + SESSION_MAX_DURATION_MS),
};
const [dbSession] = await db.insert(sessionTable).values(session).returning();
return dbSession;
}
验证 session token
这部分主要是验证 session token 是否有效,如果有效则返回 session 和 user。
如果 session 过期,则删除 session。
如果 session 快要过期并且达到了之前设定的刷新时间,则刷新 session。
import { db } from '@/db';
import { sessionTable } from '@/db/schema';
import { eq } from 'drizzle-orm';
export async function validateSessionToken(sessionToken: string): Promise<SessionValidationResult> {
let fresh = false;
const result = await db.query.sessionTable.findFirst({
where: eq(sessionTable.id, sessionToken),
with: {
user: true,
},
});
if (!result) {
return { session: null, user: null, fresh };
}
const { user, ...session } = result;
// 如果 session 过期,删除 session
if (Date.now() >= session.expiresAt.getTime()) {
await db.delete(sessionTable).where(eq(sessionTable.id, sessionToken));
return { session: null, user: null, fresh };
}
// 如果 session 快要过期,刷新 session
if (Date.now() >= session.expiresAt.getTime() - SESSION_REFRESH_INTERVAL_MS) {
session.expiresAt = new Date(Date.now() + SESSION_MAX_DURATION_MS);
// 设置为 fresh 表示 session 被刷新了
fresh = true;
await db.update(sessionTable).set({ expiresAt: session.expiresAt }).where(eq(sessionTable.id, sessionToken));
}
return { session, user, fresh };
}
删除 session
import { db } from '@/db';
import { sessionTable } from '@/db/schema';
import { eq } from 'drizzle-orm';
export async function invalidateSession(sessionId: string): Promise<void> {
await db.delete(sessionTable).where(eq(sessionTable.id, sessionId));
}
lib/lucia/cookie.ts
这部分主要是生成 cookie
的逻辑。
因为我们需要将 session
的 token
存储在 cookie
中。
如果你使用的是 Nextjs
生成后的 cookie
你就可以使用 next/headers
去设置 cookie
。
基础类型
export interface CookieAttributes {
secure?: boolean;
path?: string;
domain?: string;
sameSite?: 'lax' | 'strict' | 'none';
httpOnly?: boolean;
maxAge?: number;
expires?: Date;
}
export interface SessionCookie {
name: string;
value: string;
attributes: CookieAttributes;
}
export const SESSION_COOKIE_NAME = 'session';
const cookieOptions: CookieAttributes = {
httpOnly: true,
sameSite: 'lax' as const,
secure: process.env.NODE_ENV === 'production',
path: '/',
};
生成 cookie
export function createCookie(cookieValue: string, expiresAt: Date): SessionCookie {
return {
name: SESSION_COOKIE_NAME,
value: cookieValue,
attributes: { ...cookieOptions, expires: expiresAt },
};
}
export function createBlankCookie(): SessionCookie {
return {
name: SESSION_COOKIE_NAME,
value: '',
attributes: { ...cookieOptions, maxAge: 0 },
};
}
最后
是不是很简单?其实对于 Lucia
的迁移,作者已经给出了一个非常详细的指南。
https://lucia-auth.com/sessions/migrate-lucia-v3
如果你想要对接第三方登录的话可以查看作者的另外一个库:Arctic
https://arcticjs.dev/
lucia/index.ts 完整代码
import { db } from '@/db';
import { sessionTable } from '@/db/schema';
import { Session } from '@/zod/session';
import { SelectUser } from '@/zod/user';
import { eq } from 'drizzle-orm';
import { generateIdFromEntropySize, hashToken } from './crypto';
export type SessionValidationResult =
| { session: Session; user: SelectUser; fresh: boolean }
| { session: null; user: null; fresh: boolean };
const SESSION_REFRESH_INTERVAL_MS = 1000 * 60 * 60 * 24 * 15; // 15 days
const SESSION_MAX_DURATION_MS = SESSION_REFRESH_INTERVAL_MS * 2; // 30 days
export function generateSessionToken(): string {
const token = generateIdFromEntropySize(20);
return hashToken(token);
}
export async function createSession(sessionToken: string, userId: string): Promise<Session> {
const session = {
id: sessionToken,
userId,
expiresAt: new Date(Date.now() + SESSION_MAX_DURATION_MS),
};
const [dbSession] = await db.insert(sessionTable).values(session).returning();
return dbSession;
}
export async function validateSessionToken(sessionToken: string): Promise<SessionValidationResult> {
let fresh = false;
const result = await db.query.sessionTable.findFirst({
where: eq(sessionTable.id, sessionToken),
with: {
user: true,
},
});
if (!result) {
return { session: null, user: null, fresh };
}
const { user, ...session } = result;
// if the session is expired, delete it
if (Date.now() >= session.expiresAt.getTime()) {
await db.delete(sessionTable).where(eq(sessionTable.id, sessionToken));
return { session: null, user: null, fresh };
}
// if the session is about to expire, refresh it
if (Date.now() >= session.expiresAt.getTime() - SESSION_REFRESH_INTERVAL_MS) {
session.expiresAt = new Date(Date.now() + SESSION_MAX_DURATION_MS);
fresh = true;
await db.update(sessionTable).set({ expiresAt: session.expiresAt }).where(eq(sessionTable.id, sessionToken));
}
return { session, user, fresh };
}
export async function invalidateSession(sessionId: string): Promise<void> {
await db.delete(sessionTable).where(eq(sessionTable.id, sessionId));
}