Appearance
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 应用程序的安全性,保护敏感数据,并防止常见的安全漏洞。