我如何构建一个安全优先的 SaaS Boilerplate,拥有 100% 测试覆盖率

发布: (2025年12月18日 GMT+8 18:55)
10 min read
原文: Dev.to

Source: Dev.to

我们以后再加安全。

如果你曾经这么说过,你并不孤单。我也有过同样的经历。但在目睹了无数数据泄露、凌晨 2 点的紧急安全补丁,以及每次泄露平均 $4.45 million 的成本后,我决定采取不同的做法。

这就是我如何构建 ShipSecure 的故事——一个安全不是事后考虑,而是基石的 Next.js 样板项目。

问题:安全作为待办事项

大多数开发者处理安全的方式如下:

  • ✅ 构建着陆页
  • ✅ 添加身份验证
  • ✅ 集成支付功能
  • 安全(我们稍后再处理)

听起来熟悉吗?问题在于稍后往往意味着第一次事故之后。到那时,你已经:

  • 失去客户信任
  • 面临潜在的监管罚款(GDPR、CCPA、SOC 2)
  • 花费数周时间将安全性改装到原本未为其设计的架构中

解决方案:安全优先的开发

与其把安全当作一个功能,我把它当作 架构。每一行代码都考虑了安全,每个安全特性都有相应的测试。

下面带你了解关键概念。

1. 安全响应头 – 你的第一道防线

大多数开发者都知道 Content‑Security‑Policy,但有多少人真正正确地实现它?这不仅仅是添加一个响应头,而是需要 完整的安全响应头策略

标头防止的攻击
Content‑Security‑PolicyXSS 攻击、代码注入
Strict‑Transport‑Security协议降级攻击
X‑Content‑Type‑OptionsMIME‑嗅探漏洞
X‑Frame‑Options点击劫持攻击
Referrer‑Policy信息泄露
Permissions‑Policy未授权的浏览器功能访问

难点在于?让这些响应头协同工作而不破坏应用。单独的 CSP 就有数十个指令需要精细配置——一个错误的设置就会导致 OAuth 流程中断、分析停止或样式不加载。

我的做法: 一个集中式的 getSecurityHeaders() 函数,返回配置好的响应头对象,并通过中间件在所有路由上统一使用。

// lib/securityHeaders.ts
export function getSecurityHeaders() {
  return {
    "Content-Security-Policy":
      "default-src 'self'; script-src 'self' https://trusted.cdn.com",
    "Strict-Transport-Security":
      "max-age=63072000; includeSubDomains; preload",
    "X-Content-Type-Options": "nosniff",
    "X-Frame-Options": "DENY",
    "Referrer-Policy": "no-referrer",
    "Permissions-Policy": "geolocation=(), microphone=()",
  };
}

2. 限流 – “慢一点”的艺术

如果没有限流,你的 API 就会成为以下攻击的敞开邀请:

  • 🔓 登录暴力破解
  • 💥 DDoS 攻击
  • 🎭 凭证填充

生产环境 vs. 开发环境
生产环境需要分布式限流(例如 Upstash Redis),这样限制才能跨无服务器实例生效。开发环境则不应强制依赖 Redis。

我的解决方案: 采用混合架构并自动回退。

// lib/rateLimiter.ts
import { Redis } from "@upstash/redis";

let store: RateLimitStore;

if (process.env.UPSTASH_REDIS_URL) {
  const redis = new Redis({
    url: process.env.UPSTASH_REDIS_URL,
    token: process.env.UPSTASH_REDIS_TOKEN,
  });
  store = new RedisStore(redis);
} else {
  store = new MemoryStore(); // 简单的内存映射
}

// 导出使用滑动窗口算法的限流中间件
export const rateLimiter = createRateLimiter({
  store,
  window: 60, // 秒
  limit: 100, // 每窗口请求数
  algorithm: "sliding",
});

为什么选滑动窗口?
固定窗口会让攻击者在边界“突发”(例如在 :59 发 10 条请求,在 :01 再发 10 条)。滑动窗口消除了这种边缘情况。

3. 测试驱动的安全 – 改变游戏规则的关键

每个安全特性都有对应的测试。

重要性

  • 测试证明声明。 任何人都可以说“我们使用 CSP”。测试才能验证它。
  • 测试防止回归。 实习生不小心删掉响应头也能被捕获。
  • 测试记录行为。 安全需求变成可执行的规范。

三层测试策略

层级 1:单元测试 (Vitest)
  • 各个安全函数
  • 验证逻辑
  • 边界情况

层级 2:集成测试
  • 中间件行为
  • 认证流程
  • API 保护

层级 3:端到端测试 (Playwright)
  • 真正的浏览器行为
  • 响应头校验
  • 用户旅程安全

结果?75+ 条测试 覆盖了所有安全机制。CI/CD 在每次 PR 时运行它们——没有安全特性会在没有覆盖的情况下上线。

4. I

Source:

输入验证 – 大多数漏洞的起点

SQL 注入、XSS、命令注入——全部源于 未验证的输入

典型(易受攻击)的代码

// ❌ This is asking for trouble
const { email, name } = await req.json();
await db.users.create({ email, name });

没有验证,没有类型检查,没有防护。

安全优先的做法

  1. 为每个输入 定义 schema
  2. 在边界处 进行验证(API 路由、表单提交)。
  3. 快速失败 并返回明确的错误信息。
  4. 永不信任——即使是已认证的用户也可能发送恶意数据。

我使用 Zod 来获得编译时的类型安全和运行时的验证,但工具本身不如 随时随地验证一切 的纪律重要。

// lib/schemas.ts
import { z } from "zod";

export const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
});
// pages/api/users.ts
import { createUserSchema } from "@/lib/schemas";

export async function POST(req: Request) {
  const data = await req.json();
  const parsed = createUserSchema.safeParse(data);
  if (!parsed.success) {
    return new Response(JSON.stringify(parsed.error), { status: 400 });
  }
  await db.users.create(parsed.data);
  return new Response(null, { status: 201 });
}

5. 正确的身份验证

身份验证是大多数安全漏洞的聚集地:密码存储、会话管理、CSRF 防护、OAuth 流程等。

我的做法: 使用 Auth.js v5(前称 NextAuth),因为它提供:

  • ✅ 默认 HttpOnly、Secure Cookie
  • ✅ 内置 CSRF 防护
  • ✅ 经受考验的 OAuth 实现
  • ✅ 可靠的会话管理

关键洞察:身份验证不仅仅是登录/登出——它还涉及:

  • 会话的存储与验证方式
  • Cookie 配置(SameSite、Secure、HttpOnly)
  • 令牌的处理与轮换
  • 真正使会话失效的正确登出流程

Auth.js 开箱即用地处理上述所有内容,降低关键失误的风险。

来之不易的教训

在构建完之后,我希望自己早些知道以下内容:

1. 在设计时加入安全性可省下 10× 成本

在已有代码库中后期加入安全性非常痛苦。每一个未考虑安全的决定,日后都要付出高昂的修复代价。

(…其余教训将在原文中继续。)

2. 测试是你最好的安全文档

当有人问“你如何防止 X ?”时,指向测试文件。那是事实的证明,而不是空口承诺。

3. 开发者体验很重要

如果安全措施令人烦恼,开发者会寻找变通办法。

  • 速率限制的内存回退?这关乎开发者体验。
  • 集中的 Header 函数?这也是为了开发者体验。

安全性应对使用你代码库的开发者保持透明。

4. 一切自动化

在 CI/CD 中加入安全测试,意味着没有人能意外(或故意)发布不安全的代码。这不是信任的问题,而是系统的问题。

关键结论

事实如下:

  • 60 % 的初创公司在安全漏洞后 6 个月内关闭
  • 73 % 的企业客户要求安全认证
  • SOC 2 合规可将销售周期缩短 40 %

安全不是一个功能——它是竞争优势。

想要完整实现吗?

我已将所学全部打包成 ShipSecure —— 一个从第一天起就安全的 Next.js 15 样板项目:

  • 🛡️ 所有 7 项安全头已预配置并可用
  • ⚡ 使用 Redis + 内存回退的速率限制
  • 🔐 Auth.js v5,使用安全默认配置
  • ✅ 超过 75 条测试,覆盖 100 % 安全
  • 📦 包含 Stripe 集成
  • 📚 完整文档
  • 🔄 终身更新

一次性购买。省去安全研究,直接开始构建。

你负责开发,我们负责安全。

👉 shipsecure.dev

对 Next.js 的安全有疑问吗?留下评论——我会阅读每一条。

Back to Blog

相关文章

阅读更多 »

使用 one-shot AI 时遇到的错误

在一次性 AI 中遇到的错误 1. 无法解析 @glimmer/application.json ✘ 错误:无法解析 '@glimmer/application.json' 插件 embroider-esbui...