改变 Lucia Auth 最新 API 的内部实现

39 阅读3分钟

项目动机

  1. Next Auth 太复杂,自己的个人项目不想用到Oauth这样的高级特性。只想用email/username 和 password的组合。
  2. lucia-auth.com/ v3 简单方便实现传统的 email/username 和 password 组合。
  3. lucia-auth v3 预设 session 的 id 是 string 类型。与自己常用数据库自带的自增id差异比较大。改造起来比较麻烦。
  4. 在go 世界中用惯了 entgo.io 里的数据库生产,更改工具。希望别的项目也能按照这个工具的风格来定义表的结构 。
  5. lucia-auth 一个月前改版,V3 成为不推荐的库,成为学习类项目。本人个人项目中用的v3 将来不能使用,这样有动力去看一下 API 实现的源码。发现自己用自增id实现同样的API 难度不大。自己造半个轮子。
  6. svelte 5 自带的 sv 工具可以生成 lucia auth 最新的 API 脚手架代码。本人可以在此基础上造半个轮子。

项目使用的技术

  1. svelte 5 sv (其实可以使用任何全栈框架,next/nuxt)
  2. postgresql
  3. pg 库 (SQL 驱动)
  4. drizzle (ORM 库),用 Prisma 应该也是类似。过几天会改造另外一个用 prisma 当orm的项目。
  5. Typescript
  6. lucia auth 提供的基本authentication 库

项目代码

github.com/benluo/luci…

主要改造的代码

  1. schema 定义符合 entgo 的风格 src/lib/server/db/schema.ts
import { foreignKey, integer, pgTable, text, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: integer().primaryKey().generatedByDefaultAsIdentity({
    name: 'users_id_seq',
    startWith: 1,
    increment: 1,
    cache: 1
    }),
  username: text('username').notNull().unique(),
  password: text('password').notNull()
});

export const sessions = pgTable(
  'sessions',
  {
    id: integer().primaryKey().generatedByDefaultAsIdentity({
      name: 'sessions_id_seq',
      startWith: 1,
      increment: 1,
      cache: 1
      }),
      userSessions: integer('user_sessions').notNull(),
      sessionId: text('session_id').notNull(),
      expiresAt: timestamp('expires_at', { withTimezone: true, mode: 'date' }).notNull()
  },
  (table) => {
    return {
      sessionsUsersSessions: foreignKey({
        columns: [table.userSessions],
        foreignColumns: [users.id],
        name: 'sessions_users_sessions'
        })
       };
    }
);

export type Session = typeof sessions.$inferSelect;
export type User = typeof users.$inferSelect;

其中的几个关键点:

  • id 使用自增
  • 库名使用带s的复数形式
  • sessions库中原来id是由token生成的string 类型,现在把它增加为 sessionID 列。
  • 改变外键的命名规范,符合 entgo 的风格。
  1. 根据 schema的风格和lucia的原理,改变api的内部实现

src/lib/server/auth.ts

  • 让数据库自己生成自增id,并返回生成好的 session 记录。
export async function createSession(token: string, userId: number) {
	const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
	return await db
		.insert(table.sessions)
		.values({ userSessions: userId, sessionId, expiresAt: new Date(Date.now() + DAY_IN_MS * 30) })
		.returning();
}
  • 比较session时做相应的改变,table.sessions.sessionID 与 传入的 token 生成的id做比较。
export async function validateSessionToken(token: string) {
	const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
	const [result] = await db
		.select({
			// Adjust user table here to tweak returned data
			user: { id: table.users.id, username: table.users.username },
			session: table.sessions
		})
		.from(table.sessions)
		.innerJoin(table.users, eq(table.sessions.userSessions, table.users.id))
		.where(eq(table.sessions.sessionId, sessionId));

	if (!result) {
		return { session: null, user: null };
	}
	const { session, user } = result;

	const sessionExpired = Date.now() >= session.expiresAt.getTime();
	if (sessionExpired) {
		await db.delete(table.sessions).where(eq(table.sessions.sessionId, session.sessionId));
		return { session: null, user: null };
	}

	const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15;
	if (renewSession) {
		session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30);
		await db
			.update(table.sessions)
			.set({ expiresAt: session.expiresAt })
			.where(eq(table.sessions.sessionId, session.sessionId));
	}

	return { session, user };
}

export type SessionValidationResult = Awaited<ReturnType<typeof validateSessionToken>>;

export async function invalidateSession(sessionId: string) {
	await db.delete(table.sessions).where(eq(table.sessions.sessionId, sessionId));
}

结论

正如 lucia auth 作者所述,写一个lib需要考虑的东西太多,众口难调。当底层函数足够好用,设计思路清晰的情况下,给造轮子的人有了更灵活和坚实的基础。