Skip to content
On this page

Prisma 安全

Prisma 提供了多种安全特性来保护您的数据库和应用程序。本指南将详细介绍如何安全地使用 Prisma,包括输入验证、权限控制、数据保护等方面。

输入验证和防止注入攻击

参数化查询

Prisma 自动使用参数化查询,防止 SQL 注入:

typescript
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

// 安全:Prisma 自动参数化所有输入
async function findUserByEmail(userInput: string) {
  // 即使 userInput 包含恶意 SQL,也会被安全处理
  const user = await prisma.user.findUnique({
    where: {
      email: userInput, // 安全,会被参数化
    },
  })
  return user
}

// 危险示例:使用原生查询时需要格外小心
async function unsafeNativeQuery(userInput: string) {
  // 危险:如果 userInput 包含 ' OR 1=1 --,可能导致意外结果
  const users = await prisma.$queryRaw`SELECT * FROM User WHERE email = ${userInput}`
  return users
}

// 安全:使用参数化原生查询
async function safeNativeQuery(userInput: string) {
  const users = await prisma.$queryRaw`
    SELECT * FROM "User" WHERE "email" = ${userInput}
  `
  return users
}

输入验证中间件

typescript
// 创建输入验证中间件
import { Prisma } from '@prisma/client'

const validateInputMiddleware: Prisma.Middleware = async (params, next) => {
  // 验证电子邮件格式
  if (params.model === 'User' && params.action === 'create') {
    const email = params.args?.data?.email
    if (email && !isValidEmail(email)) {
      throw new Error('Invalid email format')
    }
  }

  // 验证字符串长度
  if (params.model === 'Post' && params.action === 'create') {
    const title = params.args?.data?.title
    if (title && title.length > 255) {
      throw new Error('Title too long')
    }
  }

  // 继续执行原始操作
  return next(params)
}

// 应用中间件
prisma.$use(validateInputMiddleware)

function isValidEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return emailRegex.test(email)
}

数据访问控制

基于角色的访问控制

typescript
// RBAC 服务
interface UserSession {
  userId: number
  roles: string[]
  permissions: string[]
}

class RBACService {
  constructor(private prisma: PrismaClient) {}

  async hasPermission(session: UserSession, resource: string, action: string): Promise<boolean> {
    // 检查用户权限
    const requiredPermission = `${resource}:${action}`
    return session.permissions.includes(requiredPermission)
  }

  async filterAccessiblePosts(session: UserSession) {
    let whereClause: any = {}

    if (!session.roles.includes('admin')) {
      // 非管理员只能看到自己的帖子或公开帖子
      whereClause = {
        OR: [
          { authorId: session.userId }, // 自己的帖子
          { published: true },          // 公开的帖子
        ],
      }
    }

    return await this.prisma.post.findMany({
      where: whereClause,
    })
  }
}

// 使用 RBAC 服务
const rbacService = new RBACService(prisma)

async function getAccessiblePosts(session: UserSession) {
  return await rbacService.filterAccessiblePosts(session)
}

行级安全

typescript
// 实现行级安全
class RowLevelSecurity {
  constructor(private prisma: PrismaClient) {}

  // 获取用户可访问的资源
  async getUserResources(userId: number, resourceType: string) {
    switch (resourceType) {
      case 'posts':
        return await this.prisma.post.findMany({
          where: {
            OR: [
              { authorId: userId },           // 用户自己的帖子
              { 
                collaborators: {             // 协作者的帖子
                  some: { id: userId }
                }
              },
              { published: true },           // 公开帖子
            ],
          },
        })
      
      case 'documents':
        return await this.prisma.document.findMany({
          where: {
            OR: [
              { ownerId: userId },           // 用户拥有的文档
              { 
                sharedWith: {               // 共享给用户的文档
                  some: { 
                    userId, 
                    permissions: { has: 'read' }
                  }
                }
              },
            ],
          },
        })
      
      default:
        throw new Error(`Unsupported resource type: ${resourceType}`)
    }
  }

  // 检查资源访问权限
  async canAccessResource(userId: number, resourceId: number, resourceType: string): Promise<boolean> {
    const accessibleResources = await this.getUserResources(userId, resourceType)
    return accessibleResources.some(resource => resource.id === resourceId)
  }
}

敏感数据保护

数据脱敏

typescript
// 数据脱敏服务
class DataMaskingService {
  // 脱敏电子邮件
  maskEmail(email: string): string {
    const [localPart, domain] = email.split('@')
    const maskedLocal = localPart.substring(0, 2) + '*'.repeat(Math.max(0, localPart.length - 2))
    return `${maskedLocal}@${domain}`
  }

  // 脱敏电话号码
  maskPhone(phone: string): string {
    const cleaned = phone.replace(/\D/g, '')
    const length = cleaned.length
    if (length >= 4) {
      return '*'.repeat(length - 4) + cleaned.slice(-4)
    }
    return '*'.repeat(length)
  }

  // 脱敏信用卡号
  maskCreditCard(cardNumber: string): string {
    const cleaned = cardNumber.replace(/\D/g, '')
    if (cleaned.length >= 4) {
      return '****-****-****-' + cleaned.slice(-4)
    }
    return cardNumber
  }
}

// 脱敏中间件
const maskingMiddleware: Prisma.Middleware = async (params, next) => {
  const result = await next(params)
  
  if (params.action.startsWith('find') && params.model === 'User') {
    // 对查询结果进行脱敏处理
    if (Array.isArray(result)) {
      return result.map(user => this.maskUserData(user))
    } else if (result && typeof result === 'object') {
      return this.maskUserData(result)
    }
  }
  
  return result
}

private maskUserData(user: any) {
  const maskingService = new DataMaskingService()
  return {
    ...user,
    email: maskingService.maskEmail(user.email),
    phone: user.phone ? maskingService.maskPhone(user.phone) : null,
  }
}

加密敏感数据

typescript
import crypto from 'crypto'

// 数据加密服务
class EncryptionService {
  private algorithm = 'aes-256-gcm'
  private secretKey = process.env.ENCRYPTION_KEY!
  
  constructor() {
    if (!this.secretKey) {
      throw new Error('ENCRYPTION_KEY environment variable is required')
    }
  }

  encrypt(text: string): { encrypted: string; iv: string; authTag: string } {
    const iv = crypto.randomBytes(16)
    const cipher = crypto.createCipher(this.algorithm, Buffer.from(this.secretKey))
    cipher.setAAD(Buffer.from('PrismaEncryption'))
    
    let encrypted = cipher.update(text, 'utf8', 'hex')
    encrypted += cipher.final('hex')
    
    const authTag = cipher.getAuthTag().toString('hex')
    
    return {
      encrypted,
      iv: iv.toString('hex'),
      authTag,
    }
  }

  decrypt(encryptedData: { encrypted: string; iv: string; authTag: string }): string {
    const decipher = crypto.createDecipher(this.algorithm, Buffer.from(this.secretKey))
    decipher.setAAD(Buffer.from('PrismaEncryption'))
    decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'))
    
    let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8')
    decrypted += decipher.final('utf8')
    
    return decrypted
  }
}

// 使用加密服务
class SecureDataService {
  private encryptionService = new EncryptionService()

  async createSecureRecord(data: { 
    userId: number; 
    sensitiveData: string 
  }) {
    const encryptedData = this.encryptionService.encrypt(data.sensitiveData)
    
    return await prisma.secureRecord.create({
      data: {
        userId: data.userId,
        encryptedData: encryptedData.encrypted,
        iv: encryptedData.iv,
        authTag: encryptedData.authTag,
      },
    })
  }

  async getSecureRecord(recordId: number, userId: number) {
    const record = await prisma.secureRecord.findUnique({
      where: {
        id: recordId,
        userId, // 确保用户只能访问自己的记录
      },
    })

    if (!record) {
      throw new Error('Record not found or access denied')
    }

    const decryptedData = this.encryptionService.decrypt({
      encrypted: record.encryptedData,
      iv: record.iv,
      authTag: record.authTag,
    })

    return decryptedData
  }
}

安全配置

数据库连接安全

typescript
// 安全的数据库配置
const secureDatabaseConfig = {
  // 使用环境变量存储连接字符串
  url: process.env.DATABASE_URL!,
  
  // 连接池配置
  connectionLimit: 10,
  
  // 启用 SSL 连接
  ssl: {
    rejectUnauthorized: true,
  },
  
  // 请求超时
  requestTimeout: 30000,
}

// 安全的 Prisma 客户端初始化
const createSecurePrismaClient = () => {
  const prisma = new PrismaClient({
    datasources: {
      db: {
        url: process.env.DATABASE_URL,
      },
    },
    // 启用查询日志(在开发环境中)
    log: process.env.NODE_ENV === 'development' 
      ? ['query', 'info', 'warn'] 
      : ['error'],
  })

  // 添加安全中间件
  prisma.$use(sensitiveDataProtectionMiddleware)
  prisma.$use(rateLimitingMiddleware)

  return prisma
}

// 敏感数据保护中间件
const sensitiveDataProtectionMiddleware: Prisma.Middleware = async (params, next) => {
  // 防止 SELECT * 查询(除非明确需要)
  if (params.action.startsWith('find') && !params.args?.select && !params.args?.include) {
    console.warn(`Unrestricted query detected: ${params.model}.${params.action}`)
  }

  // 执行原始操作
  const result = await next(params)

  // 如果在生产环境中,对敏感数据进行额外处理
  if (process.env.NODE_ENV === 'production') {
    return sanitizeResult(result, params.model)
  }

  return result
}

function sanitizeResult(result: any, model: string) {
  // 根据模型类型清理结果
  if (model === 'User') {
    if (Array.isArray(result)) {
      return result.map(item => sanitizeUser(item))
    } else {
      return sanitizeUser(result)
    }
  }
  return result
}

function sanitizeUser(user: any) {
  if (!user) return user
  // 移除敏感字段
  const { password, salt, ...safeUser } = user
  return safeUser
}

错误处理和日志安全

安全的错误处理

typescript
// 安全错误处理
class SecurityErrorHandler {
  static handleDatabaseError(error: any) {
    // 记录完整错误信息到安全日志
    this.logSecurityEvent('database_error', {
      message: error.message,
      stack: error.stack,
      timestamp: new Date().toISOString(),
    })

    // 返回通用错误消息给客户端
    if (error.code?.startsWith('P')) {
      // Prisma 数据库错误
      return new Error('Database operation failed')
    }
    
    // 其他错误
    return new Error('An unexpected error occurred')
  }

  private static logSecurityEvent(type: string, data: any) {
    // 记录到安全日志系统
    console.error(`[SECURITY] ${type}:`, JSON.stringify(data))
  }
}

// 使用安全错误处理
async function safeUserOperation(userId: number) {
  try {
    const user = await prisma.user.findUnique({
      where: { id: userId },
    })
    return user
  } catch (error) {
    // 使用安全错误处理
    throw SecurityErrorHandler.handleDatabaseError(error)
  }
}

审计日志

操作审计

typescript
// 审计日志中间件
const auditMiddleware: Prisma.Middleware = async (params, next) => {
  const startTime = Date.now()
  
  try {
    const result = await next(params)
    
    // 记录成功的操作
    await logAuditEvent({
      userId: getCurrentUserId(), // 从请求上下文获取
      action: params.action,
      model: params.model,
      targetId: getTargetId(params),
      status: 'success',
      duration: Date.now() - startTime,
      timestamp: new Date().toISOString(),
    })
    
    return result
  } catch (error) {
    // 记录失败的操作
    await logAuditEvent({
      userId: getCurrentUserId(),
      action: params.action,
      model: params.model,
      targetId: getTargetId(params),
      status: 'failed',
      error: error.message,
      duration: Date.now() - startTime,
      timestamp: new Date().toISOString(),
    })
    
    throw error
  }
}

// 审计日志服务
class AuditService {
  async logEvent(event: AuditEvent) {
    // 记录到专门的审计表
    await prisma.auditLog.create({
      data: event,
    })
  }
  
  async getUserActivity(userId: number, daysBack: number = 30) {
    const since = new Date()
    since.setDate(since.getDate() - daysBack)
    
    return await prisma.auditLog.findMany({
      where: {
        userId,
        timestamp: {
          gte: since,
        },
      },
      orderBy: {
        timestamp: 'desc',
      },
    })
  }
}

interface AuditEvent {
  userId: number
  action: string
  model: string
  targetId?: number
  status: 'success' | 'failed'
  error?: string
  duration: number
  timestamp: string
}

安全最佳实践

环境变量和配置

typescript
// 安全的配置管理
class SecurityConfig {
  static validateEnvironment() {
    const requiredVars = [
      'DATABASE_URL',
      'ENCRYPTION_KEY',
      'JWT_SECRET',
    ]

    const missing = requiredVars.filter(varName => !process.env[varName])
    
    if (missing.length > 0) {
      throw new Error(`Missing required environment variables: ${missing.join(', ')}`)
    }

    // 验证加密密钥强度
    if (process.env.ENCRYPTION_KEY && process.env.ENCRYPTION_KEY.length < 32) {
      throw new Error('ENCRYPTION_KEY must be at least 32 characters long')
    }
  }

  static isProduction(): boolean {
    return process.env.NODE_ENV === 'production'
  }

  static getDatabaseUrl(): string {
    if (!process.env.DATABASE_URL) {
      throw new Error('DATABASE_URL is not configured')
    }
    return process.env.DATABASE_URL
  }
}

// 在应用启动时验证配置
SecurityConfig.validateEnvironment()

通过实施这些安全措施,您可以显著提高 Prisma 应用程序的安全性,保护敏感数据,并防止常见的安全漏洞。