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 的对比

特性SessionJWT
状态有状态(服务端存储)无状态(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)
  }
 
  // 执行操作
})
setCookie(c, 'sessionId', sessionId, {
  httpOnly: true,      // JavaScript 无法访问,防止 XSS
  secure: true,        // 只通过 HTTPS 传输
  sameSite: 'Strict',  // 防止 CSRF
  maxAge: 24 * 60 * 60,
  path: '/',
  domain: 'example.com',
})
  • httpOnly:防止 XSS 攻击读取 Cookie
  • secure:防止中间人窃听
  • 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 应用,边缘环境需要外部存储。

这一节涉及到的几个实践:

  1. Session 原理:服务端存储映射,Cookie 传递 Session ID
  2. 存储后端:内存(开发)、Redis(生产)、KV/D1(Workers)
  3. 中间件封装:统一验证逻辑
  4. 安全风险:Session 固定、CSRF、XSS
  5. Cookie 安全:httpOnly、secure、sameSite
  6. 选择建议:Web 用 Session,API 用 JWT

Session 和 JWT 各有优劣。传统 Web 应用用 Session,分布式系统和边缘环境用 JWT。AI 项目通常是混合架构,根据场景选择。

下一篇看 OAuth 登录——第三方登录流程、GitHub/Google 集成、账号关联。