TL;DR: 安全不是一项功能特性,而是整个架构的属性。本指南涵盖生产环境 SaaS 应用的安全实践:基于密码哈希与会话管理的身份认证、基于角色和属性的授权、静态与传输中的数据加密、带 CSRF 和频率限制的 API 安全,以及持续的安全漏洞监控。
身份认证:身份层
基于会话与基于令牌的身份认证
| 方面 | 会话认证 | JWT 认证 | 混合认证(推荐) |
|---|---|---|---|
| 存储 | 服务端(D1/Redis) | 客户端(localStorage) | 服务端 + 客户端 |
| 过期 | 服务端管理 | 自包含 | 双重过期 |
| 撤销 | 即时撤销 | 困难(依赖过期时间) | 会话失效 + JWT 刷新 |
| 扩展性 | 每次请求查库 | 无状态 | 缓存会话 |
| XSS 风险 | 较低(HTTP-Only Cookie) | 较高(JS 可访问) | HTTP-Only Cookie 会话 |
使用 Better Auth 实现
// 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)
// 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 都需进行数据隔离:
// 确保所有查询都限定在用户所属组织
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 设计阶段就考虑加密列和密钥管理:
// 存储到 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 与安全响应头
// 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 安全
频率限制
// 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 防护
// TanStack Start 通过服务端函数内置了 CSRF 防护
// 所有 POST/PUT/DELETE 服务端函数在请求中包含 CSRF 令牌
// CSRF 令牌由客户端 SDK 自动附加:
createServerFn({ method: "POST" })
.handler(async ({ request }) => {
// request.headers 包含 X-CSRF-Token
// TanStack Start 自动验证
})
API 输入验证
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 监控与可观测性 技术栈中,可确保实时检测和响应安全事件:
// 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 自动)
- 配置会话过期与轮换
依赖安全
# 定期漏洞扫描
npm audit
# 或使用 GitHub Dependabot 自动拉取请求
# 需保持更新的关键包:
# - better-auth(身份认证)
# - @tanstack/react-router(路由中间件)
# - @tanstack/react-query(数据获取)
# - drizzle-orm(数据库)
# - zod(输入验证)
总结
SaaS 应用的安全是一个分层问题。你无法通过单一库或配置文件来解决它——它需要对技术栈每一层都给予持续关注:
- 身份认证:强密码哈希、会话管理、MFA
- 授权:基于角色或属性的访问控制
- 数据保护:加密、安全响应头、输入验证
- API 安全:频率限制、CSRF、CORS
- 监控:审计日志、异常检测、依赖扫描
好消息是:大多数安全实践只需实现一次即可通用应用。配置良好的身份认证库处理身份认证,中间件层处理授权,安全响应头在单一配置文件中设置。如需安全部署 SaaS 应用的完整演练,请参阅 TanStack Start 部署指南。