边缘应用的安全模型和传统服务器不一样:没有内网、没有防火墙、每个请求都暴露在公网,但好在每个请求也都经过你的 Worker。这篇把渡劫code 的四件套讲清楚:JWT 会话、KV 限流、XSS 转义、CORS 白名单——全部是线上在跑的真实代码。

JWT 会话:无状态令牌 + 短有效期

用户登录后签发 JWT(HS256),载荷只放必要的 sub、exp、role。验签在 Worker 里用 Web Crypto 完成,零依赖:

async function verifyJwt(req: Request, env: Env): Promise<Payload | null> {
  const auth = req.headers.get('Authorization') || '';
  const token = auth.replace(/^Bearer /, '');
  try {
    const [header, payload, sig] = token.split('.');
    const key = await crypto.subtle.importKey(
      'raw', new TextEncoder().encode(env.JWT_SECRET),
      { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
    const ok = await crypto.subtle.verify(
      'HMAC', key, b64urlDecode(sig),
      new TextEncoder().encode(`${header}.${payload}`));
    if (!ok) return null;
    const claims = JSON.parse(atob(payload));
    return claims.exp * 1000 > Date.now() ? claims : null;
  } catch { return null; }
}

三条纪律:密钥只进 Cloudflare Secrets 不进代码(GitHub push protection 会拦截明文,见三端同步进阶);access token 短有效期(小时级),敏感操作重新鉴权;不在载荷里放敏感数据——JWT 是 Base64 不是加密,谁都看得见。

限流:KV 计数桶

匿名接口(评论、登录尝试)按访客指纹限流,指纹 = sha256(IP + User-Agent + salt),KV 桶带 expirationTtl 自动过期:

app.use('/api/blog/*', async (c, next) => {
  const fp = await fingerprint(c.req.raw, c.env);
  const allowed = await rateLimit(c.env, `comment:${fp}`, 2, 60); // 2 条/分钟
  if (!allowed) return c.json({ code: 429, message: '操作太频繁,缓一缓' }, 429);
  await next();
});

KV 是最终一致的,精确限额请叠加 D1 的 UNIQUE 约束双保险——评论的 UV 去重就是靠 UNIQUE(slug, visitor) 在数据库层兜底,见 D1 实战。

XSS:转义是默认动作

我们的原则是能 textContent 就不 innerHTML,必须拼 HTML 时强制过 esc()。博客前端渲染评论、搜索结果等所有用户内容,统一走这一个函数:

function esc(s) {
  return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
    return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
  });
}
// 用法:el.innerHTML = '<b>' + esc(userInput) + '</b>';

五个字符一个不漏——漏了单引号,onclick= 就能从属性里逃出来。富文本场景(如果未来支持 Markdown 评论)再引入白名单净化,绝不用黑名单过滤。

CORS:白名单 + 预检缓存

博客跑在三个域名下(CF Pages、GitHub Pages、Gitee Pages),全部调用同一个 API 根。CORS 策略按 Origin 白名单放行,未命中的一律不回 CORS 头,让浏览器自己拦:

const ALLOWED_ORIGINS = [
  'https://www.520691.xyz',
  'https://gjsvip.github.io',
  'https://gitee.com',
];

app.use('/api/*', async (c, next) => {
  const origin = c.req.header('Origin') || '';
  if (ALLOWED_ORIGINS.some((o) => origin === o || origin.startsWith(o + '/'))) {
    c.res.headers.set('Access-Control-Allow-Origin', origin);
    c.res.headers.set('Vary', 'Origin');              // 防缓存串站
    c.res.headers.set('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
    c.res.headers.set('Access-Control-Allow-Headers', 'Content-Type,Authorization');
    c.res.headers.set('Access-Control-Max-Age', '86400'); // 预检缓存一天
  }
  if (c.req.method === 'OPTIONS') return c.body(null, 204);
  await next();
});

注意 Vary: Origin——CDN 缓存响应时若不带 vary,A 站的 CORS 头会被缓存给 B 站用。同时不要把 Allow-Origin 设成 * 又允许携带凭证,浏览器直接拒绝这种组合。

刷新机制用「双令牌」:access token 两小时过期,refresh token 七天、只用于换新且可被服务端吊销(D1 记录 jti 黑名单)。这样即便 access token 被截获,攻击窗口也只有两小时;用户侧无感,因为前端在收到 401 时自动用 refresh token 续期。

输入校验:第一道闸门

所有写接口先过 Schema 校验再碰数据库:字段类型、长度上限、必填项、白名单枚举。评论接口的规则是——昵称 1–24 字、正文 1–1000 字、邮箱选填但格式必须对、parent_id 必须是同一篇文章下已审核的评论。校验失败直接 400,不让脏数据有到达 D1 的机会。SQL 注入在 D1 上几乎不可能(预编译参数绑定是唯一写法),但长度炸弹与编码攻击依然要靠这一层挡掉。

日志与审计

安全设施是否在工作,要靠日志说话。Worker 对以下事件打点:登录成功与失败、限流触发、JWT 验签失败、CORS 拒绝。日志只记指纹与路径,绝不记令牌与敏感表单内容——日志本身也是攻击面。用 wrangler tail --format pretty 实时观察,异常模式(同一指纹连续失败、陌生 Origin 爆发)一目了然。

泄露响应的纪律只有一句:怀疑泄露,立即轮换。JWT_SECRET 一换,所有旧令牌自然失效,代价是全员重新登录——比起被盗号,这个代价便宜得像免费。密钥只进 Cloudflare Secrets,本地 .dev.vars 加进 .gitignore,提交前的预检钩子再兜一层。

纵深:其他该有的件

  • CSP:default-src 'self';我们用内联 style 属性,故 style-src 需带 'unsafe-inline'——这是功能与安全的显式取舍,脚本侧必须严格 'self'
  • HSTS:Cloudflare 面板开启,全站强制 HTTPS(友链申请也要求对方 HTTPS,见友链页)
  • 安全响应头:X-Content-Type-Options: nosniff、Referrer-Policy: strict-origin-when-cross-origin
  • 密钥卫生:所有令牌走 Secrets;本地日志打印前脱敏;泄露即轮换,不赌「没人看」

Worker 层的硬限制

安全策略再严密,也怕一个慢查询把整个 Worker 拖到超时。Cloudflare 对 Worker 有明确的资源红线:请求体大小上限、单请求 CPU 时间(超限直接 502)、子请求数量。实践准则:给每个下游调用设置超时与兜底值,重计算(如慢哈希)要么挪进 Durable Objects,要么直接用 Web Crypto 原生实现;数据库查询全部走索引,批量接口的 slugs 数量截断。可用性本身就是安全的一部分——一个总是 502 的接口,不需要被攻击就已经输了。

小结

边缘安全的优势是「所有流量必经一个关卡」:JWT 在入口验、限流在中间件拦、输入在闸门查、输出在渲染层转、跨域在响应头管。五层各自独立、互不依赖——任何一层被绕过,其他层还在。这套四件套没有用到任何商业产品,全部是 Cloudflare 原生能力加一个 Worker 中间件——安全的成本,有时候只是把默认配置改成正确的配置。而安全最大的敌人从来不是技术难度,是侥幸心理:每一个「应该没事」,都是未来的事故报告。