Edge Middleware 实战模式:认证、A/B 测试与个性化在边缘的正确实现

深入解析 Edge Middleware 在生产环境中的 5 大实战模式:认证鉴权、A/B 测试、地理路由、请求改写与限流防护,附 Cloudflare Workers 和 Vercel Edge 完整代码示例。

前端开发 2026-06-06 18 分钟

你已经知道边缘计算能降低延迟,但真正让 Edge Middleware 成为生产级架构核心的,不是「快」这个字——而是它能在请求到达你的源站之前,完成认证、路由、个性化和实验分流等关键决策。根据 Vercel 2025 年度报告,使用 Edge Middleware 的应用平均首字节时间(TTFB)降低了 47%,而 Cloudflare 的数据显示,边缘认证比传统中心化认证的 P99 延迟低 3-5 倍。但大多数开发者把 Edge Middleware 当成一个简单的 URL 重写工具,白白浪费了它最强大的能力。本文将用真实代码拆解 5 个生产级模式,帮你把 Edge Middleware 用到极致。

🔐 一、边缘认证:在请求到达源站前拦截未授权访问

1.1 为什么认证应该在边缘完成?

传统的认证流程是:请求 → 源站 → 验证 Token → 返回响应。在全球部署的场景下,一个亚洲用户访问美东源站的认证请求,仅网络往返就需要 200-300ms。如果把认证逻辑移到边缘节点,这个延迟可以降到 5-20ms。

但边缘环境有严格的限制——无状态、执行时间短(通常 < 50ms)、不能访问传统数据库。这意味着你的认证方案必须适配这些约束。

⚠️ **警告:**永远不要在 Edge Middleware 中做数据库查询或调用慢速外部服务。边缘节点的 CPU 时间预算极其有限,超时会直接返回错误。

1.2 JWT 验证 + JWKS 缓存的边缘实现

最常见的边缘认证模式是 JWT 验证。关键挑战是:如何在边缘获取和缓存签名密钥(JWKS)?

// Cloudflare Worker: 边缘 JWT 认证中间件
// 使用 Web Crypto API 验证 JWT,无需第三方依赖

const JWKS_URL = 'https://auth.example.com/.well-known/jwks.json';
const CACHE_TTL = 3600; // 1 小时缓存

export default {
  async fetch(request, env) {
    const authResult = await authenticateRequest(request, env);
    if (!authResult.success) {
      return new Response(JSON.stringify({ error: authResult.error }), {
        status: 401,
        headers: { 'Content-Type': 'application/json' }
      });
    }
    
    // 将用户信息注入请求头,传递给源站
    const modifiedRequest = new Request(request);
    modifiedRequest.headers.set('X-User-Id', authResult.userId);
    modifiedRequest.headers.set('X-User-Roles', JSON.stringify(authResult.roles));
    
    return fetch(modifiedRequest);
  }
};

async function authenticateRequest(request, env) {
  const authHeader = request.headers.get('Authorization');
  if (!authHeader?.startsWith('Bearer ')) {
    return { success: false, error: 'Missing token' };
  }
  
  const token = authHeader.slice(7);
  
  try {
    // 解码 JWT header 获取 kid
    const [headerB64] = token.split('.');
    const header = JSON.parse(atob(headerB64));
    
    // 从缓存或远程获取 JWKS
    const jwks = await getJWKS(env);
    const key = jwks.keys.find(k => k.kid === header.kid);
    if (!key) return { success: false, error: 'Unknown key' };
    
    // 使用 Web Crypto API 导入公钥
    const publicKey = await crypto.subtle.importKey(
      'jwk', key,
      { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
      false, ['verify']
    );
    
    // 验证签名
    const [headerPart, payloadPart, signaturePart] = token.split('.');
    const encoder = new TextEncoder();
    const data = encoder.encode(`${headerPart}.${payloadPart}`);
    const signature = Uint8Array.from(atob(signaturePart), c => c.charCodeAt(0));
    
    const valid = await crypto.subtle.verify('RSASSA-PKCS1-v1_5', publicKey, signature, data);
    if (!valid) return { success: false, error: 'Invalid signature' };
    
    // 解析 payload 并检查过期
    const payload = JSON.parse(atob(payloadPart));
    if (payload.exp && payload.exp < Date.now() / 1000) {
      return { success: false, error: 'Token expired' };
    }
    
    return { success: true, userId: payload.sub, roles: payload.roles || [] };
  } catch (e) {
    return { success: false, error: 'Token verification failed' };
  }
}

// 使用 Cloudflare KV 缓存 JWKS,避免每次请求都远程获取
async function getJWKS(env) {
  const cached = await env.JWKS_CACHE.get('jwks', { type: 'json' });
  if (cached) return cached;
  
  const response = await fetch(JWKS_URL);
  const jwks = await response.json();
  await env.JWKS_CACHE.put('jwks', JSON.stringify(jwks), { expirationTtl: CACHE_TTL });
  return jwks;
}

1.3 基于路径的认证策略

不是所有路径都需要认证。一个常见的模式是「白名单 + 默认拒绝」:

// Vercel Edge Middleware: 基于路径的认证策略
// middleware.ts 放在项目根目录

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

// 公开路径白名单
const PUBLIC_PATHS = [
  '/api/health',
  '/api/auth/login',
  '/api/auth/register',
  '/login',
  '/register',
  '/about',
  '/pricing',
];

// 路径前缀白名单
const PUBLIC_PREFIXES = [
  '/_next/',      // Next.js 静态资源
  '/images/',     // 图片资源
  '/favicon',     // favicon
  '/robots.txt',
  '/sitemap.xml',
];

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  
  // 检查是否为公开路径
  if (PUBLIC_PATHS.includes(pathname)) {
    return NextResponse.next();
  }
  
  // 检查是否为公开前缀
  if (PUBLIC_PREFIXES.some(prefix => pathname.startsWith(prefix))) {
    return NextResponse.next();
  }
  
  // 验证认证 token
  const token = request.cookies.get('session_token')?.value;
  if (!token) {
    // API 路径返回 401,页面路径重定向到登录
    if (pathname.startsWith('/api/')) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('redirect', pathname);
    return NextResponse.redirect(loginUrl);
  }
  
  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

💡 **提示:**Vercel Edge Middleware 运行在 Edge Runtime 上,不支持 Node.js API(如 fscrypto)。如果你需要 crypto 操作,使用 Web Crypto API 或在 middleware 中只做轻量级的 token 格式校验,将真正的签名校验留给 API Route。

🚀 二、A/B 测试与渐进式发布:边缘分流的正确姿势

2.1 为什么 A/B 测试应该在边缘做?

传统的 A/B 测试通常在客户端(JavaScript)实现,这会导致页面闪烁(Flicker)——用户先看到原始版本,然后才切换到实验版本。在边缘做分流可以确保用户从第一次请求开始就看到正确的版本,同时避免客户端 A/B 测试脚本对性能的影响。

根据 Google 的研究,页面加载每增加 100ms,转化率下降 1%。客户端 A/B 测试脚本通常增加 50-200ms 的加载时间,而边缘分流几乎零开销。

2.2 Cookie 持久化的边缘分流实现

// Cloudflare Worker: A/B 测试边缘分流
// 确保同一用户始终看到同一个实验版本

const EXPERIMENTS = {
  'new-checkout': {
    variants: ['control', 'variant-a', 'variant-b'],
    weights: [0.34, 0.33, 0.33],  // 流量分配权重
    cookieName: 'exp_new_checkout',
    cookieMaxAge: 60 * 60 * 24 * 30, // 30 天
  },
  'pricing-page-v2': {
    variants: ['control', 'variant'],
    weights: [0.5, 0.5],
    cookieName: 'exp_pricing_v2',
    cookieMaxAge: 60 * 60 * 24 * 14, // 14 天
  }
};

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const cookies = parseCookies(request.headers.get('Cookie') || '');
    
    const experimentResults = {};
    
    for (const [expName, config] of Object.entries(EXPERIMENTS)) {
      // 检查是否已有实验 cookie(用户已分组)
      const existingVariant = cookies[config.cookieName];
      
      if (existingVariant && config.variants.includes(existingVariant)) {
        experimentResults[expName] = existingVariant;
      } else {
        // 新用户:按权重随机分配
        experimentResults[expName] = assignVariant(config.variants, config.weights);
      }
    }
    
    // 构建修改后的请求
    const modifiedRequest = new Request(request);
    
    // 将实验分组信息注入请求头
    modifiedRequest.headers.set('X-Experiments', JSON.stringify(experimentResults));
    
    // 如果需要改写路径(如将 /pricing 指向 /pricing-v2)
    const variant = experimentResults['pricing-page-v2'];
    if (url.pathname === '/pricing' && variant === 'variant') {
      const newUrl = new URL('/pricing-v2', url.origin);
      modifiedRequest.headers.set('X-Rewrite-To', newUrl.pathname);
    }
    
    const response = await fetch(modifiedRequest);
    
    // 设置实验 cookie(确保后续请求保持同一版本)
    const newResponse = new Response(response.body, response);
    
    for (const [expName, variant] of Object.entries(experimentResults)) {
      const config = EXPERIMENTS[expName];
      const existingCookie = cookies[config.cookieName];
      
      if (existingCookie !== variant) {
        newResponse.headers.append(
          'Set-Cookie',
          `${config.cookieName}=${variant}; Path=/; Max-Age=${config.cookieMaxAge}; SameSite=Lax; Secure`
        );
      }
    }
    
    // 添加调试头(生产环境可移除)
    newResponse.headers.set('X-Experiments', JSON.stringify(experimentResults));
    
    return newResponse;
  }
};

// 按权重随机分配变体
function assignVariant(variants, weights) {
  const random = Math.random();
  let cumulative = 0;
  for (let i = 0; i < variants.length; i++) {
    cumulative += weights[i];
    if (random < cumulative) return variants[i];
  }
  return variants[variants.length - 1];
}

function parseCookies(cookieStr) {
  return Object.fromEntries(
    cookieStr.split(';').map(c => c.trim().split('=').map(s => s.trim()))
  );
}

📌 **记住:**A/B 测试的 cookie 必须设置 HttpOnlySecure 标志。在上面的示例中,实验 cookie 不需要 HttpOnly(因为前端 JS 也可能需要读取),但生产环境中应根据安全需求调整。

2.3 分流方案对比

方案 延迟影响 闪烁问题 SEO 影响 实现复杂度 推荐场景
✅ Edge Middleware 分流 < 5ms 可控制 服务端渲染页面、需要 SEO 的页面
✅ 客户端 JS 分流 50-200ms 有风险 SPA 应用、不需要 SEO 的内部页面
⚠️ DNS 分流 0ms 复杂 地理级别的大范围实验
❌ 源站分流 50-300ms 可控制 不推荐(增加源站负载)

🌍 三、地理路由与请求改写:基于位置的智能分发

3.1 地理感知路由的核心逻辑

Edge Middleware 可以读取用户的地理位置信息(通过 cf-ipcountryx-vercel-ip-country 等头部),实现基于位置的内容分发、语言切换和合规处理。

// Cloudflare Worker: 地理感知路由与内容改写
// 支持多语言、GDPR 合规、CDN 回源优化

const LOCALE_MAP = {
  'CN': { locale: 'zh-CN', currency: 'CNY', redirect: '/zh' },
  'TW': { locale: 'zh-TW', currency: 'TWD', redirect: '/zh-tw' },
  'HK': { locale: 'zh-HK', currency: 'HKD', redirect: '/zh-hk' },
  'JP': { locale: 'ja', currency: 'JPY', redirect: '/ja' },
  'KR': { locale: 'ko', currency: 'KRW', redirect: '/ko' },
  'US': { locale: 'en', currency: 'USD', redirect: '/en' },
  'GB': { locale: 'en-GB', currency: 'GBP', redirect: '/en' },
  'DE': { locale: 'de', currency: 'EUR', redirect: '/de' },
};

// GDPR 适用国家列表
const GDPR_COUNTRIES = new Set([
  'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR',
  'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL',
  'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE',
]);

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const country = request.headers.get('cf-ipcountry') || 'US';
    const localeConfig = LOCALE_MAP[country] || LOCALE_MAP['US'];
    
    // 1. 未手动选择语言的用户:自动重定向
    const acceptLang = request.headers.get('Accept-Language') || '';
    const userCookie = parseCookies(request.headers.get('Cookie') || '');
    const hasManualLocale = userCookie['locale_preference'];
    
    if (!hasManualLocale && url.pathname === '/') {
      return Response.redirect(`${url.origin}${localeConfig.redirect}`, 302);
    }
    
    // 2. GDPR 合规:注入 consent 提示
    const isGDPR = GDPR_COUNTRIES.has(country);
    
    // 3. 注入地理信息到请求头
    const modifiedRequest = new Request(request);
    modifiedRequest.headers.set('X-User-Country', country);
    modifiedRequest.headers.set('X-User-Locale', localeConfig.locale);
    modifiedRequest.headers.set('X-User-Currency', localeConfig.currency);
    modifiedRequest.headers.set('X-GDPR-Required', isGDPR ? 'true' : 'false');
    
    // 4. 价格 API 请求:改写为对应货币
    if (url.pathname.startsWith('/api/prices')) {
      url.searchParams.set('currency', localeConfig.currency);
      return fetch(new Request(url.toString(), modifiedRequest));
    }
    
    const response = await fetch(modifiedRequest);
    
    // 5. GDPR 场景:注入 Consent 头部
    if (isGDPR) {
      const newResponse = new Response(response.body, response);
      newResponse.headers.set('X-Consent-Required', 'true');
      return newResponse;
    }
    
    return response;
  }
};

function parseCookies(cookieStr) {
  return Object.fromEntries(
    cookieStr.split(';').map(c => c.trim().split('=').map(s => s.trim()))
  );
}

3.2 限流与安全防护:边缘级别的 DDoS 缓解

在边缘做限流的优势是:恶意流量在到达源站之前就被拦截。以下是基于滑动窗口的边缘限流实现:

// Cloudflare Worker: 基于滑动窗口的边缘限流
// 使用 Durable Objects 实现精确计数

export class RateLimiter {
  constructor(state, env) {
    this.state = state;
    this.env = env;
  }

  async fetch(request) {
    const { identifier, limit, windowMs } = await request.json();
    
    const now = Date.now();
    const windowStart = now - windowMs;
    
    // 获取当前窗口的请求记录
    let requests = (await this.state.storage.get(identifier)) || [];
    
    // 清除过期记录
    requests = requests.filter(timestamp => timestamp > windowStart);
    
    if (requests.length >= limit) {
      const retryAfter = Math.ceil((requests[0] + windowMs - now) / 1000);
      return new Response(JSON.stringify({
        allowed: false,
        remaining: 0,
        retryAfter,
      }), {
        status: 429,
        headers: {
          'Content-Type': 'application/json',
          'Retry-After': String(retryAfter),
        }
      });
    }
    
    // 记录本次请求
    requests.push(now);
    await this.state.storage.put(identifier, requests);
    
    return new Response(JSON.stringify({
      allowed: true,
      remaining: limit - requests.length,
      resetAt: new Date(requests[0] + windowMs).toISOString(),
    }), {
      headers: { 'Content-Type': 'application/json' }
    });
  }
}

💡 四、Edge Middleware 的性能陷阱与避坑指南

4.1 常见的性能反模式

在生产环境中,Edge Middleware 最容易踩的坑不是功能实现,而是性能优化。以下是我在实际项目中总结的 5 个高频反模式:

  • 在 middleware 中做外部 HTTP 调用 — 每个外部调用增加 50-200ms 延迟,直接抵消边缘部署的优势
  • 在 middleware 中解析完整的请求体 — 大请求体(如文件上传)会消耗大量 CPU 时间
  • 使用 npm 包而未检查 bundle 大小 — 某些 npm 包在边缘环境中会导致 cold start 时间暴增
  • 在 middleware 中写日志到远程服务 — 同步日志写入会阻塞请求处理
  • 不设置 KV/Cache 缓存 — 每次请求都做 JWT 验证中的 JWKS 获取

4.2 最佳实践清单

  • 缓存一切可缓存的数据 — JWKS、配置、feature flags 都应该缓存在 KV 或 Cache API 中
  • 使用 Web Crypto API — 边缘环境原生支持,无需引入第三方加密库
  • 保持 middleware 代码 < 1MB — 超过此大小会显著增加 cold start 时间
  • 使用 early return 模式 — 对公开路径快速放行,减少不必要的处理
  • 添加 X-Edge-Timing — 测量 middleware 执行时间,持续监控性能

⚡ **关键结论:**Edge Middleware 的最佳实践可以用一句话概括——能不在边缘做的事情就不要在边缘做。边缘的价值在于快速决策和路由,而不是复杂业务逻辑。把认证、限流、A/B 分流这些「决策型」逻辑放在边缘,把数据处理、业务计算留给源站。

🔧 五、总结与工具推荐

Edge Middleware 不是银弹,但它确实为 Web 应用架构提供了一个新的优化维度。核心价值在于:在请求到达源站之前完成关键决策,从而降低延迟、提升安全性和改善用户体验。

适用场景总结:

场景 适用度 说明
✅ JWT 认证与权限校验 轻量级验证,无需数据库
✅ A/B 测试分流 无闪烁,零客户端开销
✅ 地理路由与语言切换 利用边缘节点的地理信息
✅ 请求限流与安全防护 恶流量在边缘拦截
⚠️ 个性化内容渲染 需要用户数据时可能需要 KV 查询
❌ 复杂业务逻辑 CPU 时间限制,留给源站
❌ 数据库密集型操作 边缘无法直连传统数据库

推荐工具与平台:

  • 🔧 Cloudflare Workers — 最成熟的边缘计算平台,Durable Objects 支持有状态边缘逻辑
  • 🔧 Vercel Edge Middleware — Next.js 生态首选,零配置集成
  • 🔧 Deno Deploy — 原生 TypeScript 支持,全球 35+ 区域
  • 🔧 Fastly Compute — 基于 WebAssembly 的边缘计算,适合高性能场景
  • 🔧 Edge Config(Vercel) — 超低延迟的全球配置存储,适合 feature flags

💡 **提示:**如果你的项目已经在用 Next.js,从 Vercel Edge Middleware 开始是最小阻力路径。如果需要更灵活的控制和更低的成本,Cloudflare Workers 是更好的选择。两者都支持在 < 50ms 内完成认证、路由和分流决策。

📚 相关文章