SaaS Security Best Practices:身份认证、授权与数据保护

现代 SaaS 应用安全全面指南,涵盖身份认证策略、授权模型、数据加密、API 安全、频率限制与生产环境监控。

Priya Sharma
Priya Sharma
July 8, 202614 min read
其他语言:Deutsch · English

TL;DR: 安全不是一项功能特性,而是整个架构的属性。本指南涵盖生产环境 SaaS 应用的安全实践:基于密码哈希与会话管理的身份认证、基于角色和属性的授权、静态与传输中的数据加密、带 CSRF 和频率限制的 API 安全,以及持续的安全漏洞监控。


身份认证:身份层

基于会话与基于令牌的身份认证

方面会话认证JWT 认证混合认证(推荐)
存储服务端(D1/Redis)客户端(localStorage)服务端 + 客户端
过期服务端管理自包含双重过期
撤销即时撤销困难(依赖过期时间)会话失效 + JWT 刷新
扩展性每次请求查库无状态缓存会话
XSS 风险较低(HTTP-Only Cookie)较高(JS 可访问)HTTP-Only Cookie 会话

使用 Better Auth 实现

typescript
// src/lib/auth.ts — 使用 Better Auth 与 Drizzle
import { betterAuth } from "better-auth"
import { drizzleAdapter } from "better-auth/adapters/drizzle"
import { createDb } from "../db"

export const auth = betterAuth({
  database: drizzleAdapter(createDb(env), {
    provider: "sqlite",
  }),
  emailAndPassword: {
    enabled: true,
    autoSignIn: true,
    passwordHash: {
      algorithm: "argon2", // Argon2id — OWASP 推荐
      params: {
        memoryCost: 19456,
        timeCost: 2,
        parallelism: 1,
      },
    },
  },
  socialProviders: {
    google: { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET },
    github: { clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET },
  },
  session: {
    expiresIn: 7 * 24 * 60 * 60, // 7 天
    updateAge: 24 * 60 * 60, // 每 24 小时刷新
  },
})

密码安全检查清单

  • 使用 Argon2id 哈希密码(非 bcrypt,非 scrypt)
  • 最少 8 个字符,无任意复杂性规则
  • 登录尝试限速(每 IP 每分钟 5 次)
  • 首次登录前必须完成邮箱验证
  • 会话令牌存储在 HTTP-Only、Secure、SameSite=Strict Cookie 中
  • 登录和权限升级时进行会话轮换
  • 提供 MFA(基于 TOTP 或 WebAuthn)
  • 10 次失败尝试后锁定账户

授权:谁能做什么

基于角色的访问控制(RBAC)

typescript
// src/lib/auth.ts
export const roles = {
  admin: {
    permissions: [
      "user:*",
      "subscription:*",
      "billing:*",
      "settings:*",
      "analytics:*",
    ],
  },
  member: {
    permissions: [
      "user:read",
      "subscription:read",
      "settings:read",
      "settings:update",
    ],
  },
  viewer: {
    permissions: ["user:read", "subscription:read"],
  },
}

// 中间件守卫
export function requirePermission(permission: string) {
  return createServerFn({ method: "GET" }).handler(
    async ({}, { context }) => {
      const userRole = context.user.role as keyof typeof roles
      const allowed = roles[userRole]?.permissions.some(
        (p) => p === permission || p.endsWith(":*")
      )

      if (!allowed) {
        throw new Error("Forbidden")
      }
    }
  )
}

组织范围隔离

每个多用户 SaaS 都需进行数据隔离:

typescript
// 确保所有查询都限定在用户所属组织
export const getTeamSubscriptions = createServerFn({ method: "GET" }).handler(
  async ({}, { context }) => {
    // context.user.orgId 由 auth 中间件设置
    const subscriptions = await env.DB.prepare(`
      SELECT * FROM subscriptions
      WHERE organization_id = ?
      ORDER BY created_at DESC
    `).bind(context.user.orgId).all()

    return subscriptions.results
  }
)

授权模式

模式说明适用场景
RBAC基于角色的权限简单应用、小团队
ABAC基于属性(资源所有者、部门等)多租户、复杂组织
ReBAC基于关系(Google Zanzibar 模型)大规模、嵌套组织
权限数据化权限存储在数据库,运行时检查用户自定义角色

数据保护

静态加密

Cloudflare D1 默认对所有数据进行静态加密。如需额外保护,可在存储前对敏感字段进行加密。底层数据库架构应在 Schema 设计阶段就考虑加密列和密钥管理:

typescript
// 存储到 D1 前加密 PII
import { AesGcm } from "@cloudflare/workers-types"

export const encryptField = async (plaintext: string, key: CryptoKey) => {
  const iv = crypto.getRandomValues(new Uint8Array(12))
  const encoded = new TextEncoder().encode(plaintext)

  const ciphertext = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    key,
    encoded
  )

  return {
    ciphertext: arrayBufferToBase64(ciphertext),
    iv: arrayBufferToBase64(iv),
  }
}

export const decryptField = async (
  ciphertext: string,
  iv: string,
  key: CryptoKey
) => {
  const decrypted = await crypto.subtle.decrypt(
    { name: "AES-GCM", iv: base64ToArrayBuffer(iv) },
    key,
    base64ToArrayBuffer(ciphertext)
  )

  return new TextDecoder().decode(decrypted)
}

HTTPS 与安全响应头

typescript
// middleware.ts — TanStack Router 中间件
export const Route = createRootRouteWithContext()({
  beforeLoad: async ({ context }) => {
    // 安全响应头
    context.response.headers.set(
      "Content-Security-Policy",
      "default-src 'self'; script-src 'self' 'unsafe-inline' https://analytics.tanstackship.com; style-src 'self' 'unsafe-inline'"
    )
    context.response.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
    context.response.headers.set("X-Content-Type-Options", "nosniff")
    context.response.headers.set("X-Frame-Options", "DENY")
    context.response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin")
  },
})

API 安全

频率限制

typescript
// server/middleware/rate-limit.ts
import { createServerFn } from "@tanstack/react-start"

const rateLimitStore = new Map<string, { count: number; resetAt: number }>()

export function rateLimit(limit: number, windowMs: number) {
  return async (request: Request): Promise<boolean> => {
    const ip = request.headers.get("CF-Connecting-IP") ?? "unknown"
    const key = `${ip}:${request.url}`
    const now = Date.now()

    const entry = rateLimitStore.get(key)

    if (!entry || entry.resetAt < now) {
      rateLimitStore.set(key, { count: 1, resetAt: now + windowMs })
      return true
    }

    if (entry.count >= limit) {
      return false // 触发频率限制
    }

    entry.count++
    return true
  }
}

// 在服务端函数中使用
export const login = createServerFn({ method: "POST" }).handler(
  async ({ request }) => {
    const allowed = await rateLimit(5, 60_000)(request) // 每分钟 5 次尝试
    if (!allowed) {
      throw new Error("请求过于频繁,请稍后再试。")
    }
    // ... 登录逻辑
  }
)

CSRF 防护

typescript
// TanStack Start 通过服务端函数内置了 CSRF 防护
// 所有 POST/PUT/DELETE 服务端函数在请求中包含 CSRF 令牌

// CSRF 令牌由客户端 SDK 自动附加:
createServerFn({ method: "POST" })
  .handler(async ({ request }) => {
    // request.headers 包含 X-CSRF-Token
    // TanStack Start 自动验证
  })

API 输入验证

typescript
import { z } from "zod"

const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2).max(100),
  role: z.enum(["admin", "member"]).optional(),
})

export const createUser = createServerFn({ method: "POST" })
  .validator((data: unknown) => createUserSchema.parse(data))
  .handler(async ({ data }) => {
    // data 经过验证,类型为 { email: string; name: string; role?: string }
    // 绝不信任来自客户端的原始输入
  })

安全监控

事件检测

一套完善的安全监控系统应记录关键事件并在出现可疑活动时发出告警。将安全事件集成到 SaaS 监控与可观测性 技术栈中,可确保实时检测和响应安全事件:

typescript
// server/security/audit-log.ts
export const logSecurityEvent = createServerFn({ method: "POST" }).handler(
  async ({ data }: { data: SecurityEvent }) => {
    await env.DB.prepare(`
      INSERT INTO audit_log (id, user_id, event_type, ip_address, metadata, created_at)
      VALUES (?, ?, ?, ?, ?, ?)
    `).bind(
      crypto.randomUUID(),
      data.userId,
      data.eventType,
      data.ipAddress,
      JSON.stringify(data.metadata),
      Date.now()
    ).run()

    // 对可疑事件发出告警
    if (["failed_login", "password_change", "role_escalation"].includes(data.eventType)) {
      await sendAlert(data)
    }
  }
)

记录的安全事件

事件操作严重级别
login_success记录Info
login_failed记录 + 计数Medium(5 次后:High)
password_reset记录 + 通知用户Medium
email_changed记录 + 通知旧邮箱High
role_changed记录 + 通知管理员High
api_key_created记录Medium
suspicious_ip记录 + 封禁High

生产环境安全检查清单

  • 所有密码使用 Argon2id 哈希
  • 所有需认证路由必须验证会话
  • 所有数据查询限定在用户所属组织
  • 认证端点和公开 API 配置频率限制
  • CSP 响应头配置(严格模式)
  • CORS 限制为已知来源
  • 防止 SQL 注入(通过 Drizzle 使用参数化查询)
  • 防止 XSS(React 自动转义输出)
  • 启用 CSRF 防护
  • 安全审计日志及保留策略
  • 每周运行 npm audit
  • 安全补丁发布 14 天内更新依赖
  • 通过环境变量管理密钥(不提交到代码库)
  • D1 备份已配置(Cloudflare 自动)
  • 配置会话过期与轮换

依赖安全

bash
# 定期漏洞扫描
npm audit
# 或使用 GitHub Dependabot 自动拉取请求

# 需保持更新的关键包:
# - better-auth(身份认证)
# - @tanstack/react-router(路由中间件)
# - @tanstack/react-query(数据获取)
# - drizzle-orm(数据库)
# - zod(输入验证)

总结

SaaS 应用的安全是一个分层问题。你无法通过单一库或配置文件来解决它——它需要对技术栈每一层都给予持续关注:

  1. 身份认证:强密码哈希、会话管理、MFA
  2. 授权:基于角色或属性的访问控制
  3. 数据保护:加密、安全响应头、输入验证
  4. API 安全:频率限制、CSRF、CORS
  5. 监控:审计日志、异常检测、依赖扫描

好消息是:大多数安全实践只需实现一次即可通用应用。配置良好的身份认证库处理身份认证,中间件层处理授权,安全响应头在单一配置文件中设置。如需安全部署 SaaS 应用的完整演练,请参阅 TanStack Start 部署指南。