一、引言
Serverless 与边缘计算让「应用逻辑跑在边缘」,但认证会话往往是迁移到 Serverless 时最容易被忽略的环节:状态不在服务器进程里了,传统 Session 落内存的方式失效。边缘认证的核心问题是:「没有常驻服务器进程,我如何记住这个用户是谁?」
本文系统讲边缘认证与会话:先对比 JWT 与 Session 两条路线,再深入无状态 JWT 与 KV 存储会话的实现,接着讲 Cookie 安全属性与 CSRF/XSS 防护、边缘中间件鉴权(Vercel/Cloudflare 实战)、第三方 OAuth 集成,最后覆盖多租户隔离与登出失效策略。
关联:https://plumephp.com/tools-serverless-cold-start/(Serverless 运行形态)、https://plumephp.com/tools-edge-cache-cdn-strategy/(边缘缓存)、https://plumephp.com/vercel-edge-functions-guide/(边缘函数)。
二、JWT vs Session:两条路线
2.1 两种会话方案的对比
| 维度 | JWT(无状态) | Session(有状态) |
|---|---|---|
| 会话存哪里 | 客户端(Token 内) | 服务端(内存/Redis/KV) |
| 服务端查会话 | 无(验签即可) | 每次查存储 |
| 登出/失效 | 难(需黑名单) | 容易(删会话) |
| 状态撤销 | 弱 | 强 |
| 存储成本 | 无 | 有(KV/DB) |
| 适用 | 无状态 API、边缘 | 强管控 Web 应用 |
2.2 边缘场景的选择
纯边缘函数(Cloudflare Workers):无状态优先 → JWT
需要强登出/权限变更即时生效 :KV 存 Session → 每次查 KV
混合(推荐):
JWT 管「身份信息」+ KV 存「撤销名单/会话元数据」
2.3 关键决策:谁负责「验」与「查」
JWT 验签:用公钥/密钥在边缘函数内验签 → 无需查库
Session 查询:每个请求查 KV/Redis → 边缘到数据中心有延迟
因此边缘场景默认 JWT,除非有强撤销需求
一句话总结:JWT 把状态放客户端、验签即信任;Session 把状态放服务端、每次查存储——边缘场景默认 JWT,需要即时撤销再加 KV 黑名单。
三、无状态 JWT 的完整实现
3.1 JWT 结构
Header.Payload.Signature
Header : { "alg": "HS256", "typ": "JWT" }
Payload: { "sub": "user-123", "role": "admin", "exp": 1730000000, "iat": 1729996400 }
签名 : HMAC(header.payload, secret) (或 RS256 用私钥签、公钥验)
3.2 边缘函数签发与校验
// Cloudflare Workers:签发 JWT(示意)
import { SignJWT, jwtVerify } from 'hono/jwt'
// 登录成功 → 签发
const token = await new SignJWT({ sub: user.id, role: user.role })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('2h')
.sign(secret)
// 每次请求 → 验签
async function authenticate(req: Request) {
const token = getCookie(req, 'session') || bearerToken(req)
try {
const { payload } = await jwtVerify(token, secret)
return payload // { sub, role }
} catch {
return null // 过期/篡改
}
}
3.3 RS256(公钥验签)与多运行时
HS256:对称密钥,同密钥签发+校验 → 适合单信任域
RS256:私钥签发、公钥验签 → 适合多服务/第三方验证(无共享密钥)
边缘验证用 RS256 公钥:任何边缘函数都能验,密钥不泄露
一句话总结:JWT 落地 = 登录签一个带 exp 的 token,请求时验签取身份;HS256 简单、RS256 适合多服务公钥验签。
四、Cookie 安全属性与防护
4.1 Cookie 的四个安全属性
// 登录后设置会话 Cookie
return new Response('ok', {
headers: {
'Set-Cookie': `session=${token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=7200`
}
})
| 属性 | 作用 | 说明 |
|---|---|---|
| HttpOnly | 禁止 JS 读取 | 防 XSS 窃取 Token |
| Secure | 仅 HTTPS 发送 | 防明文传输 |
| SameSite=Lax | 限制跨站携带 | 防 CSRF |
| Max-Age/Expires | 过期时间 | 控制会话时长 |
4.2 CSRF 防护
SameSite=Lax:绝大多数跨站请求不带 Cookie → 挡住 CSRF
高危写操作再加:CSRF Token(双重提交)+ Origin 校验
// 双保险:校验 Origin + 自定义头
function csrfGuard(req: Request) {
const origin = req.headers.get('Origin')
const allowed = new Set(['https://app.example.com'])
if (origin && !allowed.has(origin)) return false
// 自定义头要求:fetch 必须带 X-Requested-With(浏览器不会自动带)
return !!req.headers.get('X-Requested-With')
}
4.3 XSS 防护
HttpOnly 挡「读 Cookie」,但挡不住「执行脚本」
XSS 防护组合:
1. 输出转义(前端框架默认)
2. CSP(Content-Security-Policy)限制脚本来源
3. 敏感操作二次验证(密码/短信)
一句话总结:Cookie 四属性是边缘会话的「地基」——HttpOnly 挡 XSS 读、Secure 挡明文、SameSite 挡 CSRF,写操作再加 Origin 校验与 CSRF Token。
五、边缘中间件鉴权实战
5.1 Vercel Middleware 统一鉴权
// Vercel:Middleware 在边缘对所有请求做鉴权
import { NextResponse } from 'next/server'
import { jwtVerify } from 'hono/jwt'
export async function middleware(request: Request) {
const token = request.cookies.get('session')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
try {
const { payload } = await jwtVerify(token, process.env.JWT_SECRET!)
request.headers.set('x-user-id', payload.sub as string)
return NextResponse.next({ request })
} catch {
return NextResponse.redirect(new URL('/login', request.url))
}
}
export const config = {
matcher: ['/dashboard/:path*', '/api/private/:path*'] // 只拦截敏感路径
}
5.2 Cloudflare Workers 鉴权
// Cloudflare Workers:入口统一鉴权
export default {
async fetch(req: Request) {
const url = new URL(req.url)
if (url.pathname.startsWith('/admin')) {
const auth = await authenticate(req)
if (!auth) return new Response('Unauthorized', { status: 401 })
}
// 继续路由
}
}
5.3 鉴权位置的选择
边缘中间件/入口:全局兜底(未登录 → 重定向)
具体路由/API :细粒度权限(角色校验)
函数内二次校验 :敏感操作(写操作/改权限)
三层叠加,边缘兜底 + 核心再次校验
一句话总结:边缘中间件把「是否登录」前移到请求入口——Vercel Middleware 用 matcher 拦截敏感路径、Cloudflare 在 fetch 入口判断,再配合函数内角色校验双保险。
六、第三方 OAuth 集成
6.1 OAuth PKCE 流程
适合 Serverless(无需保存客户端密钥的风险方案):
1. 前端生成 code_verifier → 计算 challenge → 跳转授权
2. 授权回调 → 带 code + code_verifier 换 token
3. 用 access_token 取用户信息 → 本地签发会话
6.2 Auth.js(NextAuth)在 Vercel 落地
// NextAuth + Vercel Postgres 存会话
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
import { PostgresAdapter } from '@auth/pg-adapter'
export const { handlers, auth } = NextAuth({
adapter: PostgresAdapter(pool),
providers: [GitHub],
session: { strategy: 'database' }, // 数据库会话,登出即时生效
})
6.3 边缘环境的 OAuth 注意
1. 回调 URL 必须是稳定的公网地址(不能是临时 Preview)
2. 会话存储选 KV/数据库(数据库会话可即时登出)
3. OAuth 状态参数要防 CSRF(state 随机值 + 校验)
4. 用户信息变更后要能同步(webhook 或下次登录刷新)
一句话总结:OAuth 在 Serverless 用 PKCE 免存客户端密钥,Auth.js + 数据库适配器让会话可即时登出——关键是回调地址稳定、state 防 CSRF、会话存储可撤销。
七、多租户会话隔离
7.1 会话中的租户标识
JWT Payload 带 tenant_id → 每个请求都能判断「属于哪个租户」
数据访问必须带租户条件 → 防越权
// JWT 带租户
const token = await new SignJWT({
sub: user.id, tenant: user.tenantId, role: user.role
}).setExpirationTime('2h').sign(secret)
// 查询必须限定租户
async function getOrders(payload: { tenant: string }, req) {
return await db.query('SELECT * FROM orders WHERE tenant_id = $1', [payload.tenant])
}
7.2 会话隔离的边界
1. Cookie 名可加租户后缀,避免跨租户串会话
2. 缓存键带 tenant_id(防止一租户命中另一租户缓存)
3. 边缘中间件也可按租户路由到不同后端
4. 登出要按租户+用户精确失效
一句话总结:多租户会话的核心是「身份里带 tenant,数据访问必带 tenant 条件」——Cookie 命名、缓存键、日志全部要带租户维度防串。
八、登出与失效策略
8.1 无状态 JWT 的失效难点
JWT 发出去就「无法主动作废」(除非黑名单)
方案:
1. 短 exp(15分钟-2小时)→ 过期即失效
2. KV 黑名单:登出时把 jti 写进 KV → 每次验签查 KV(有状态了)
3. 版本号:改密/改角色时递增 user 的 auth_version → token 里带 version,不匹配即失效
// KV 黑名单登出
await KV.put(`blacklist:${jti}`, '1', { expirationTtl: ttl })
// 校验时
if (await KV.get(`blacklist:${payload.jti}`)) {
return null // 已登出
}
8.2 登出策略对比
| 方案 | 即时性 | 存储成本 | 适用 |
|---|---|---|---|
| 短 exp | 延迟 | 无 | 无状态强场景 |
| KV 黑名单 | 即时 | 每条会话一条 | 需要登出生效 |
| auth_version | 即时 | 每用户一条 | 权限变更强管控 |
8.3 「改密码即全员下线」
用户改密码 → 递增 auth_version → 所有旧 token 验签时 version 不匹配 → 全失效
这是「吊销所有已发 Token」最干净的实现
一句话总结:JWT 失效三板斧——短 exp 兜底、KV 黑名单即时登出、auth_version 权限变更全员下线,按「即时性需求」选型。
九、边缘认证的完整架构
请求 → 边缘中间件(Vercel/Cloudflare)
├─ 无 session Cookie → 302 到 /login
├─ 有 Cookie → 验签 JWT(RS256 公钥 / HS256 密钥)
│ ├─ 校验 auth_version + 黑名单
│ ├─ 通过 → 注入 x-user-id/tenant → 放行到函数
│ └─ 失败 → 401 / 清 Cookie
└─ 写操作 → 函数内再校验角色 + CSRF Token
登录 → OAuth PKCE / 密码 → 签发 JWT(短 exp)→ Set-Cookie(HttpOnly/Secure/SameSite)
登出 → 清 Cookie + KV 黑名单写 jti → 立即失效
各层的职责:
边缘中间件:全局「是否登录」兜底(快)
函数内 :角色/租户/写权限细粒度校验(准)
KV/数据库 :会话撤销与审计(可管)
一句话总结:边缘认证架构 = 边缘中间件兜底登录 + 函数内细粒度授权 + KV 会话撤销——登录签短命 JWT、安全 Cookie 传输、黑名单即时登出,三层各司其职。
十、速查表
| 需求 | 方案 |
|---|---|
| 无状态会话 | JWT(RS256 公钥验签) |
| 即时登出 | KV 黑名单 jti |
| 改密全员下线 | auth_version |
| Cookie 安全 | HttpOnly + Secure + SameSite |
| CSRF | SameSite + Origin 校验 + Token |
| XSS | 转义 + CSP + HttpOnly |
| 全局鉴权 | 边缘中间件 matcher |
| OAuth | PKCE + Auth.js + 数据库会话 |
| 多租户 | token 带 tenant,查询必带条件 |
| 敏感写操作 | 二次验证 + 角色校验 |
一句话记忆:边缘认证没有进程状态,核心是把「身份」放进 JWT 或 KV——JWT 验签即信、KV 会话可撤销;Cookie 四属性(HttpOnly/Secure/SameSite/过期)是地基,CSRF/XSS 靠 SameSite、CSP 与转义;边缘中间件兜底登录、函数内做细粒度授权、KV 做黑名单即时登出;多租户身份带 tenant、数据必带条件——认证的第一原则是「身份可验、会话可撤、数据隔离」。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。