Session 认证
要点
- Session 是有状态的认证方式,服务端需要存储会话数据
- Session ID 通过 Cookie 传递给客户端,后续请求自动携带
- Session 适合传统 Web 应用,JWT 适合 API 和分布式系统
- Session 需要存储后端(内存、Redis、数据库),边缘环境需要特殊处理
- Session 固定攻击、CSRF 攻击是需要防范的安全风险
- AI 项目中,Session 适合管理后台,API 服务更适合 JWT
1. Session 原理
1.1 流程
Session 认证的完整流程:
1. 用户提交账号密码
2. 服务端验证通过,生成 Session ID
3. 服务端存储 Session ID 和用户信息的映射
4. 通过 Set-Cookie 把 Session ID 发给浏览器
5. 浏览器后续请求自动携带 Cookie
6. 服务端从 Cookie 取出 Session ID,查询对应的用户信息1.2 与 JWT 的对比
| 特性 | Session | JWT |
|---|---|---|
| 状态 | 有状态(服务端存储) | 无状态(Token 自包含) |
| 存储 | 服务端内存/Redis/数据库 | 客户端存储 |
| 扩展性 | 需要共享 Session 存储 | 天然支持分布式 |
| 撤销 | 直接删除 Session | 需要黑名单或短过期时间 |
| 跨域 | Cookie 限制 | 灵活(Header 传递) |
| 边缘环境 | 需要外部存储 | 无需存储 |
2. Hono 实现
2.1 内存存储
开发环境可以用内存存储:
import { Hono } from 'hono'
import { getCookie, setCookie, deleteCookie } from 'hono/cookie'
import { randomBytes } from 'crypto'
const app = new Hono()
// Session 存储(内存)
const sessions = new Map<string, { userId: string; email: string; expiresAt: Date }>()
// 登录
app.post('/auth/login', async (c) => {
const { email, password } = await c.req.json()
// 验证用户
const user = await verifyUser(email, password)
if (!user) {
return c.json({ error: 'Invalid credentials' }, 401)
}
// 生成 Session ID
const sessionId = randomBytes(32).toString('hex')
// 存储 Session
sessions.set(sessionId, {
userId: user.id,
email: user.email,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 小时
})
// 设置 Cookie
setCookie(c, 'sessionId', sessionId, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'Strict',
maxAge: 24 * 60 * 60,
path: '/',
})
return c.json({ message: 'Login successful' })
})
// 受保护的路由
app.get('/api/profile', async (c) => {
const sessionId = getCookie(c, 'sessionId')
if (!sessionId) {
return c.json({ error: 'Unauthorized' }, 401)
}
const session = sessions.get(sessionId)
if (!session || session.expiresAt < new Date()) {
return c.json({ error: 'Session expired' }, 401)
}
return c.json({
userId: session.userId,
email: session.email,
})
})
// 登出
app.post('/auth/logout', (c) => {
const sessionId = getCookie(c, 'sessionId')
if (sessionId) {
sessions.delete(sessionId)
}
deleteCookie(c, 'sessionId')
return c.json({ message: 'Logout successful' })
})2.2 Redis 存储
生产环境用 Redis 存储 Session:
import { Redis } from 'ioredis'
const redis = new Redis(process.env.REDIS_URL!)
// 登录
app.post('/auth/login', async (c) => {
const { email, password } = await c.req.json()
const user = await verifyUser(email, password)
if (!user) {
return c.json({ error: 'Invalid credentials' }, 401)
}
const sessionId = randomBytes(32).toString('hex')
// 存储到 Redis,设置过期时间
await redis.setex(
`session:${sessionId}`,
24 * 60 * 60, // 24 小时
JSON.stringify({
userId: user.id,
email: user.email,
})
)
setCookie(c, 'sessionId', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'Strict',
maxAge: 24 * 60 * 60,
})
return c.json({ message: 'Login successful' })
})
// 受保护的路由
app.get('/api/profile', async (c) => {
const sessionId = getCookie(c, 'sessionId')
if (!sessionId) {
return c.json({ error: 'Unauthorized' }, 401)
}
const sessionData = await redis.get(`session:${sessionId}`)
if (!sessionData) {
return c.json({ error: 'Session expired' }, 401)
}
const session = JSON.parse(sessionData)
return c.json(session)
})2.3 中间件封装
把 Session 验证逻辑封装成中间件:
import { createMiddleware } from 'hono/factory'
type SessionVariables = {
session: { userId: string; email: string }
}
const sessionAuth = createMiddleware<{ Variables: SessionVariables }>(
async (c, next) => {
const sessionId = getCookie(c, 'sessionId')
if (!sessionId) {
return c.json({ error: 'Unauthorized' }, 401)
}
const sessionData = await redis.get(`session:${sessionId}`)
if (!sessionData) {
return c.json({ error: 'Session expired' }, 401)
}
const session = JSON.parse(sessionData)
c.set('session', session)
await next()
}
)
// 使用
app.use('/api/*', sessionAuth)
app.get('/api/profile', (c) => {
const session = c.get('session')
return c.json(session)
})3. Cloudflare Workers 中的 Session
Cloudflare Workers 是无状态的,每次请求都是新实例。Session 需要外部存储。
3.1 使用 KV 存储
import { Hono } from 'hono'
import { getCookie, setCookie } from 'hono/cookie'
type Bindings = {
SESSION_KV: KVNamespace
}
const app = new Hono<{ Bindings: Bindings }>()
app.post('/auth/login', async (c) => {
const { email, password } = await c.req.json()
const user = await verifyUser(email, password)
if (!user) {
return c.json({ error: 'Invalid credentials' }, 401)
}
const sessionId = crypto.randomUUID()
// 存储到 KV,设置过期时间
await c.env.SESSION_KV.put(
`session:${sessionId}`,
JSON.stringify({
userId: user.id,
email: user.email,
}),
{ expirationTtl: 24 * 60 * 60 } // 24 小时
)
setCookie(c, 'sessionId', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'Strict',
maxAge: 24 * 60 * 60,
})
return c.json({ message: 'Login successful' })
})
app.get('/api/profile', async (c) => {
const sessionId = getCookie(c, 'sessionId')
if (!sessionId) {
return c.json({ error: 'Unauthorized' }, 401)
}
const sessionData = await c.env.SESSION_KV.get(`session:${sessionId}`)
if (!sessionData) {
return c.json({ error: 'Session expired' }, 401)
}
return c.json(JSON.parse(sessionData))
})3.2 使用 Durable Objects
Durable Objects 可以提供强一致性的 Session 管理:
export class SessionManager {
state: DurableObjectState
constructor(state: DurableObjectState) {
this.state = state
}
async fetch(request: Request) {
const url = new URL(request.url)
if (url.pathname === '/create') {
const sessionId = crypto.randomUUID()
const data = await request.json()
await this.state.storage.put(`session:${sessionId}`, data)
return new Response(JSON.stringify({ sessionId }))
}
if (url.pathname === '/get') {
const sessionId = url.searchParams.get('sessionId')
const data = await this.state.storage.get(`session:${sessionId}`)
return new Response(JSON.stringify(data))
}
if (url.pathname === '/delete') {
const sessionId = url.searchParams.get('sessionId')
await this.state.storage.delete(`session:${sessionId}`)
return new Response(JSON.stringify({ deleted: true }))
}
return new Response('Not found', { status: 404 })
}
}4. Session 安全
4.1 Session 固定攻击
攻击者预先设置 Session ID,诱导用户使用:
1. 攻击者生成 Session ID: abc123
2. 诱导用户访问: https://example.com?sessionId=abc123
3. 用户登录,Session ID abc123 被绑定到用户账号
4. 攻击者使用 abc123 访问,获得用户权限防御:登录后重新生成 Session ID:
app.post('/auth/login', async (c) => {
// 删除旧的 Session
const oldSessionId = getCookie(c, 'sessionId')
if (oldSessionId) {
await redis.del(`session:${oldSessionId}`)
}
// 生成新的 Session ID
const newSessionId = randomBytes(32).toString('hex')
// 存储新 Session
await redis.setex(`session:${newSessionId}`, ...)
setCookie(c, 'sessionId', newSessionId, ...)
})4.2 CSRF 攻击
CSRF(Cross-Site Request Forgery)利用浏览器自动携带 Cookie 的特性:
1. 用户登录 example.com,获得 Session Cookie
2. 用户访问恶意网站 evil.com
3. evil.com 的页面自动发起请求到 example.com
4. 浏览器自动携带 example.com 的 Cookie
5. 服务端认为是合法用户,执行操作防御方式:
SameSite Cookie 属性:
setCookie(c, 'sessionId', sessionId, {
sameSite: 'Strict', // 或 'Lax'
// ...
})Strict:完全禁止第三方携带Lax:允许部分情况(如链接跳转)None:允许所有(必须配合Secure)
CSRF Token:
// 生成 CSRF Token
app.get('/api/csrf-token', (c) => {
const token = randomBytes(32).toString('hex')
// 存储到 Session
const session = c.get('session')
session.csrfToken = token
return c.json({ token })
})
// 验证 CSRF Token
app.post('/api/transfer', async (c) => {
const { csrfToken } = await c.req.json()
const session = c.get('session')
if (csrfToken !== session.csrfToken) {
return c.json({ error: 'Invalid CSRF token' }, 403)
}
// 执行操作
})4.3 Cookie 安全属性
setCookie(c, 'sessionId', sessionId, {
httpOnly: true, // JavaScript 无法访问,防止 XSS
secure: true, // 只通过 HTTPS 传输
sameSite: 'Strict', // 防止 CSRF
maxAge: 24 * 60 * 60,
path: '/',
domain: 'example.com',
})httpOnly:防止 XSS 攻击读取 Cookiesecure:防止中间人窃听sameSite:防止 CSRF 攻击
5. Session vs JWT 选择
5.1 选 Session 的场景
- 传统 Web 应用,前端是浏览器
- 需要立即撤销会话(如修改密码后强制登出)
- 服务端有稳定的存储(Redis、数据库)
- 不需要跨域
5.2 选 JWT 的场景
- API 服务,前端是 SPA 或移动端
- 分布式系统,不想共享 Session 存储
- 边缘环境(Workers、Deno Deploy)
- 需要跨域
5.3 AI 项目的选择
AI 项目通常是混合架构:
- 管理后台:Session 认证(传统 Web)
- API 服务:JWT 认证(无状态)
- WebSocket:JWT 或 Session 都可以
// 管理后台用 Session
app.use('/admin/*', sessionAuth)
// API 用 JWT
app.use('/api/*', jwtAuth)总结
Session 是有状态的认证方式,服务端需要存储会话数据。适合传统 Web 应用,边缘环境需要外部存储。
这一节涉及到的几个实践:
- Session 原理:服务端存储映射,Cookie 传递 Session ID
- 存储后端:内存(开发)、Redis(生产)、KV/D1(Workers)
- 中间件封装:统一验证逻辑
- 安全风险:Session 固定、CSRF、XSS
- Cookie 安全:httpOnly、secure、sameSite
- 选择建议:Web 用 Session,API 用 JWT
Session 和 JWT 各有优劣。传统 Web 应用用 Session,分布式系统和边缘环境用 JWT。AI 项目通常是混合架构,根据场景选择。
下一篇看 OAuth 登录——第三方登录流程、GitHub/Google 集成、账号关联。