现代 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 并进行性能优化。