JWT 认证
Hono 内置了 JWT 中间件(hono/jwt),基于 Web Crypto API 实现,无需任何 Node.js 依赖,在所有运行时上均可使用:
# 安装(如需密码哈希)
bun add bcryptjs
bun add -D @types/bcryptjs
完整登录注册流程
// src/routes/auth.ts
import { Hono } from 'hono'
import { sign, verify } from 'hono/jwt'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import bcrypt from 'bcryptjs'
type Env = {
Bindings: { JWT_SECRET: string; DB: D1Database }
Variables: { userId: string; userEmail: string }
}
const auth = new Hono<Env>()
const registerSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
password: z.string().min(8, '密码至少8位')
.regex(/[A-Z]/, '需要包含大写字母')
.regex(/[0-9]/, '需要包含数字'),
})
// 注册
auth.post('/register', zValidator('json', registerSchema), async (c) => {
const { name, email, password } = c.req.valid('json')
// 检查邮箱是否已注册
const existing = await c.env.DB
.prepare('SELECT id FROM users WHERE email = ?')
.bind(email).first()
if (existing) {
return c.json({ error: 'Email already registered' }, 409)
}
// 哈希密码(salt rounds = 12)
const passwordHash = await bcrypt.hash(password, 12)
// 创建用户
const user = await c.env.DB
.prepare('INSERT INTO users (name, email, password_hash) VALUES (?, ?, ?) RETURNING id, name, email')
.bind(name, email, passwordHash).first<{ id: string; name: string; email: string }>()
// 生成 Access Token(短期)
const accessToken = await sign(
{ sub: user!.id, email: user!.email, exp: Math.floor(Date.now() / 1000) + 15 * 60 }, // 15分钟
c.env.JWT_SECRET
)
// 生成 Refresh Token(长期)
const refreshToken = await sign(
{ sub: user!.id, type: 'refresh', exp: Math.floor(Date.now() / 1000) + 7 * 24 * 3600 }, // 7天
c.env.JWT_SECRET
)
return c.json({ user: { id: user!.id, name: user!.name, email: user!.email }, accessToken, refreshToken }, 201)
})
// 登录
auth.post('/login', zValidator('json', z.object({
email: z.string().email(),
password: z.string(),
})), async (c) => {
const { email, password } = c.req.valid('json')
const user = await c.env.DB
.prepare('SELECT id, name, email, password_hash FROM users WHERE email = ?')
.bind(email)
.first<{ id: string; name: string; email: string; password_hash: string }>()
if (!user || !(await bcrypt.compare(password, user.password_hash))) {
// 故意不区分"用户不存在"和"密码错误",防止用户枚举
return c.json({ error: 'Invalid email or password' }, 401)
}
const accessToken = await sign(
{ sub: user.id, email: user.email, exp: Math.floor(Date.now() / 1000) + 15 * 60 },
c.env.JWT_SECRET
)
return c.json({ user: { id: user.id, name: user.name, email: user.email }, accessToken })
})
export default auth
JWT 验证中间件
// middleware/jwt-auth.ts
import { createMiddleware } from 'hono/factory'
import { verify } from 'hono/jwt'
export const jwtAuth = createMiddleware<{
Bindings: { JWT_SECRET: string }
Variables: { userId: string; userEmail: string }
}>(async (c, next) => {
const authHeader = c.req.header('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return c.json({ error: 'Missing or invalid Authorization header' }, 401)
}
const token = authHeader.slice(7)
try {
const payload = await verify(token, c.env.JWT_SECRET)
c.set('userId', payload.sub as string)
c.set('userEmail', payload.email as string)
await next()
} catch {
return c.json({ error: 'Invalid or expired token' }, 401)
}
})
Rate Limiting(基于 KV)
// middleware/rate-limit.ts
import { createMiddleware } from 'hono/factory'
type RateLimitOptions = {
windowSeconds: number
max: number
keyFn?: (c: any) => string
}
export const rateLimit = (opts: RateLimitOptions) =>
createMiddleware<{ Bindings: { RATE_LIMIT_KV: KVNamespace } }>(async (c, next) => {
const ip = c.req.header('CF-Connecting-IP') ?? 'unknown'
const key = opts.keyFn ? opts.keyFn(c) : `rl:${c.req.path}:${ip}`
const current = await c.env.RATE_LIMIT_KV.get(key)
const count = current ? Number(current) : 0
if (count >= opts.max) {
c.header('Retry-After', String(opts.windowSeconds))
c.header('X-RateLimit-Limit', String(opts.max))
c.header('X-RateLimit-Remaining', '0')
return c.json({ error: 'Too many requests, please try again later' }, 429)
}
// 原子计数(使用 expirationTtl 实现滑动窗口)
await c.env.RATE_LIMIT_KV.put(key, String(count + 1), {
expirationTtl: opts.windowSeconds,
})
c.header('X-RateLimit-Limit', String(opts.max))
c.header('X-RateLimit-Remaining', String(opts.max - count - 1))
await next()
})
// 使用
app.use('/api/auth/*', rateLimit({ windowSeconds: 60, max: 10 })) // 每分钟10次
app.use('/api/*', rateLimit({ windowSeconds: 60, max: 100 })) // 每分钟100次
CSRF 防护
import { csrf } from 'hono/csrf'
// 内置 CSRF 防护(检查 Origin/Referer Header)
app.use(csrf({
origin: ['https://myapp.com', 'https://staging.myapp.com'],
}))
// 对于纯 API(使用 Bearer Token),CSRF 威胁较低
// 对于使用 Cookie Session 的 Web 应用,CSRF 防护是必须的
安全 HTTP Headers
import { secureHeaders } from 'hono/secure-headers'
// 一键添加所有安全 Headers(类似 helmet)
app.use(secureHeaders())
// 等同于自动添加以下 Headers:
// X-Content-Type-Options: nosniff
// X-Frame-Options: DENY
// X-XSS-Protection: 0
// Referrer-Policy: no-referrer
// Permissions-Policy: ...(禁用不必要的浏览器权限)
// Strict-Transport-Security: max-age=15552000
// Content-Security-Policy: default-src 'self'
// 自定义 CSP
app.use(secureHeaders({
contentSecurityPolicy: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // 需要内联脚本时
styleSrc: ["'self'", "fonts.googleapis.com"],
imgSrc: ["'self'", "data:", "https:"],
},
}))
OAuth2 集成(Clerk)
// 使用 Clerk 处理 OAuth2/OIDC(推荐的第三方方案)
bun add @clerk/backend
// middleware/clerk.ts
import { createClerkClient } from '@clerk/backend'
import { createMiddleware } from 'hono/factory'
export const clerkAuth = createMiddleware<{
Bindings: { CLERK_SECRET_KEY: string }
Variables: { userId: string | null }
}>(async (c, next) => {
const clerk = createClerkClient({ secretKey: c.env.CLERK_SECRET_KEY })
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (token) {
try {
const session = await clerk.verifyToken(token)
c.set('userId', session.sub)
} catch {
c.set('userId', null)
}
} else {
c.set('userId', null)
}
await next()
})
安全最佳实践:(1) 永远不要在 JWT payload 中存储敏感信息(密码哈希、信用卡号),payload 是 Base64 编码,任何人都可以解码;(2) Access Token 有效期控制在 15 分钟以内;(3) 密码必须使用 bcrypt/argon2 哈希,绝不能明文存储;(4) 所有登录接口必须加 Rate Limiting;(5) 错误消息不要区分"用户不存在"和"密码错误",防止用户枚举攻击。