F FisherHub Docs

03. 数据库与认证

现代 Web 应用离不开数据库和用户认证系统。本文以 SQLite 为例,展示如何在 Hono 应用中集成 Drizzle ORM,并实现完整的 JWT 认证流程。

准备工作

安装必要的依赖:

bun add drizzle-orm better-sqlite3
bun add -d drizzle-kit @types/better-sqlite3
bun add jsonwebtoken bcryptjs
bun add -d @types/jsonwebtoken @types/bcryptjs

初始化 Drizzle 配置:

bun drizzle-kit init

在项目根目录创建 drizzle.config.ts

import type { Config } from 'drizzle-kit'

export default {
  schema: './src/db/schema.ts',
  out: './drizzle',
  dialect: 'sqlite',
  dbCredentials: {
    url: './data/app.db',
  },
} satisfies Config

定义数据库模型

// src/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  username: text('username').notNull().unique(),
  email: text('email').notNull().unique(),
  passwordHash: text('password_hash').notNull(),
  createdAt: text('created_at').notNull().default('current_timestamp'),
})

export const sessions = sqliteTable('sessions', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  userId: integer('user_id').notNull().references(() => users.id),
  token: text('token').notNull().unique(),
  expiresAt: text('expires_at').notNull(),
  createdAt: text('created_at').notNull().default('current_timestamp'),
})

运行迁移命令创建数据库表:

bun drizzle-kit push

初始化数据库连接

// src/db/index.ts
import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import * as schema from './schema'

const sqlite = new Database('./data/app.db')
sqlite.pragma('journal_mode = WAL')
sqlite.pragma('foreign_keys = ON')

export const db = drizzle(sqlite, { schema })

用户注册

注册接口接收用户名、邮箱和密码,将密码哈希后存入数据库:

import { Hono } from 'hono'
import { db } from '../db'
import { users } from '../db/schema'
import { eq } from 'drizzle-orm'
import bcrypt from 'bcryptjs'

const app = new Hono()

app.post('/api/auth/register', async (c) => {
  const { username, email, password } = await c.req.json()

  // 基础校验
  if (!username || !email || !password) {
    return c.json({ error: '请填写所有必填字段' }, 400)
  }
  if (password.length < 6) {
    return c.json({ error: '密码至少 6 个字符' }, 400)
  }

  // 检查用户名或邮箱是否已存在
  const existingUser = await db
    .select()
    .from(users)
    .where(eq(users.email, email))
    .get()

  if (existingUser) {
    return c.json({ error: '该邮箱已被注册' }, 409)
  }

  // 密码加盐哈希
  const passwordHash = await bcrypt.hash(password, 10)

  // 插入用户
  const newUser = await db
    .insert(users)
    .values({
      username,
      email,
      passwordHash,
    })
    .returning({ id: users.id, username: users.username, email: users.email })
    .get()

  return c.json({ user: newUser }, 201)
})

用户登录与 JWT 签发

登录接口验证密码后签发 JWT Token:

import jwt from 'jsonwebtoken'

const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-key-change-in-production'

app.post('/api/auth/login', async (c) => {
  const { email, password } = await c.req.json()

  if (!email || !password) {
    return c.json({ error: '请填写邮箱和密码' }, 400)
  }

  // 查找用户
  const user = await db
    .select()
    .from(users)
    .where(eq(users.email, email))
    .get()

  if (!user) {
    return c.json({ error: '邮箱或密码错误' }, 401)
  }

  // 验证密码
  const valid = await bcrypt.compare(password, user.passwordHash)
  if (!valid) {
    return c.json({ error: '邮箱或密码错误' }, 401)
  }

  // 签发 JWT
  const token = jwt.sign(
    { sub: user.id, username: user.username },
    JWT_SECRET,
    { expiresIn: '7d' }
  )

  return c.json({
    token,
    user: {
      id: user.id,
      username: user.username,
      email: user.email,
    },
  })
})

JWT 验证中间件

将 JWT 验证封装为中间件,保护需要认证的路由:

import type { MiddlewareHandler } from 'hono'
import jwt from 'jsonwebtoken'

// 为 Hono 上下文声明自定义变量
type Variables = {
  userId: number
  username: string
}

const authMiddleware: MiddlewareHandler<{ Variables: Variables }> = async (c, next) => {
  const authHeader = c.req.header('Authorization')

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return c.json({ error: '未提供认证令牌' }, 401)
  }

  const token = authHeader.slice(7)

  try {
    const payload = jwt.verify(token, JWT_SECRET) as { sub: number; username: string }
    c.set('userId', payload.sub)
    c.set('username', payload.username)
    await next()
  } catch (error) {
    return c.json({ error: '令牌无效或已过期' }, 401)
  }
}

// 使用中间件保护路由
app.get('/api/profile', authMiddleware, (c) => {
  const userId = c.get('userId')
  const username = c.get('username')
  return c.json({ userId, username })
})

完整的认证路由

将所有认证相关路由组织在一起:

// src/routes/auth.ts
import { Hono } from 'hono'
import { z } from 'zod'

const auth = new Hono()

const RegisterSchema = z.object({
  username: z.string().min(2).max(50),
  email: z.string().email(),
  password: z.string().min(6).max(100),
})

const LoginSchema = z.object({
  email: z.string().email(),
  password: z.string(),
})

auth.post('/register', async (c) => {
  const body = await c.req.json()
  const parsed = RegisterSchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: parsed.error.issues }, 400)
  }
  // ... 注册逻辑
})

auth.post('/login', async (c) => {
  const body = await c.req.json()
  const parsed = LoginSchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: parsed.error.issues }, 400)
  }
  // ... 登录逻辑
})

auth.get('/profile', authMiddleware, (c) => {
  return c.json({
    userId: c.get('userId'),
    username: c.get('username'),
  })
})

app.route('/api/auth', auth)

获取当前用户

一个常用的模式是在中间件中解析 Token,将用户信息挂载到上下文,后续的路由可以直接读取:

app.get('/api/auth/me', authMiddleware, async (c) => {
  const userId = c.get('userId')

  const user = await db
    .select({
      id: users.id,
      username: users.username,
      email: users.email,
      createdAt: users.createdAt,
    })
    .from(users)
    .where(eq(users.id, userId))
    .get()

  if (!user) {
    return c.json({ error: '用户不存在' }, 404)
  }

  return c.json({ user })
})

小结

本文介绍了 Hono 与 Drizzle ORM 的集成方式,实现了完整的用户注册、登录流程,并使用 JWT 做身份认证。这些模式是构建生产级 Web 应用的基础。下一篇文章将介绍如何将 Hono 应用部署到 Cloudflare Workers 并进行性能优化。