如何从 Server Component 直接访问后端服务(如数据库)?为什么这是安全的?

19 阅读2分钟

如何从 Server Component 直接访问后端服务(如数据库)?为什么这是安全的?

Server Component 直接访问后端

基本概念

Server Components 在服务器端运行,可以直接访问后端服务,无需通过 API 路由:

// app/users/page.js - Server Component
import { db } from '@/lib/database'

export default async function UsersPage() {
  // 直接访问数据库
  const users = await db.users.findMany({
    select: {
      id: true,
      name: true,
      email: true,
      createdAt: true,
    },
  })

  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  )
}

数据库连接示例

// lib/database.js
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis

export const db = globalForPrisma.prisma || new PrismaClient()

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = db
}
// app/products/page.js
import { db } from '@/lib/database'

export default async function ProductsPage() {
  const products = await db.products.findMany({
    include: {
      category: true,
      reviews: {
        select: {
          rating: true,
          comment: true,
        },
      },
    },
  })

  return (
    <div>
      <h1>Products</h1>
      {products.map((product) => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          <p>Category: {product.category.name}</p>
          <p>Price: ${product.price}</p>
          <div>Reviews: {product.reviews.length}</div>
        </div>
      ))}
    </div>
  )
}

为什么这是安全的?

1. 服务器端执行

// Server Component - 在服务器端执行
export default async function SecureData() {
  // 这些代码只在服务器端运行,不会发送到客户端
  const secretKey = process.env.DATABASE_URL
  const apiKey = process.env.API_SECRET_KEY

  // 直接访问数据库,无需暴露连接字符串
  const data = await db.sensitiveData.findMany()

  return (
    <div>
      {/* 只有处理后的数据会发送到客户端 */}
      {data.map((item) => (
        <div key={item.id}>
          {item.publicField} {/* 只显示公开字段 */}
        </div>
      ))}
    </div>
  )
}

2. 环境变量保护

// .env.local
DATABASE_URL = 'postgresql://user:password@localhost:5432/mydb'
API_SECRET_KEY = 'secret-key-here'
JWT_SECRET = 'jwt-secret-here'

// Server Component 可以安全访问
export default async function SecurePage() {
  // 环境变量只在服务器端可用
  const dbUrl = process.env.DATABASE_URL
  const apiKey = process.env.API_SECRET_KEY

  // 客户端无法访问这些变量
  const data = await fetchData(apiKey)

  return <div>{data}</div>
}

3. 数据过滤和验证

// app/admin/users/page.js
import { db } from '@/lib/database'
import { auth } from '@/lib/auth'

export default async function AdminUsersPage() {
  // 服务器端身份验证
  const session = await auth()

  if (!session || session.user.role !== 'admin') {
    return <div>Access denied</div>
  }

  // 只获取管理员需要的数据
  const users = await db.users.findMany({
    select: {
      id: true,
      name: true,
      email: true,
      role: true,
      lastLogin: true,
      // 不包含敏感字段如 password, ssn 等
    },
  })

  return (
    <div>
      <h1>Admin Users</h1>
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Email</th>
            <th>Role</th>
            <th>Last Login</th>
          </tr>
        </thead>
        <tbody>
          {users.map((user) => (
            <tr key={user.id}>
              <td>{user.name}</td>
              <td>{user.email}</td>
              <td>{user.role}</td>
              <td>{user.lastLogin}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

实际应用场景

1. 电商网站产品页面

// app/products/[id]/page.js
import { db } from '@/lib/database'
import { notFound } from 'next/navigation'

export default async function ProductPage({ params }) {
  const { id } = params

  // 直接查询数据库
  const product = await db.products.findUnique({
    where: { id: parseInt(id) },
    include: {
      category: true,
      reviews: {
        include: {
          user: {
            select: { name: true },
          },
        },
      },
      inventory: true,
    },
  })

  if (!product) {
    notFound()
  }

  // 计算平均评分
  const avgRating =
    product.reviews.reduce((sum, review) => sum + review.rating, 0) /
    product.reviews.length

  return (
    <div>
      <h1>{product.name}</h1>
      <p>Price: ${product.price}</p>
      <p>Category: {product.category.name}</p>
      <p>Rating: {avgRating.toFixed(1)}</p>
      <p>Stock: {product.inventory.quantity}</p>

      <div>
        <h2>Reviews</h2>
        {product.reviews.map((review) => (
          <div key={review.id}>
            <p>
              {review.user.name}: {review.rating}/5
            </p>
            <p>{review.comment}</p>
          </div>
        ))}
      </div>
    </div>
  )
}

2. 博客文章页面

// app/blog/[slug]/page.js
import { db } from '@/lib/database'
import { notFound } from 'next/navigation'

export default async function BlogPost({ params }) {
  const { slug } = params

  // 直接查询数据库
  const post = await db.posts.findUnique({
    where: { slug },
    include: {
      author: {
        select: { name: true, avatar: true },
      },
      tags: true,
      comments: {
        include: {
          author: {
            select: { name: true },
          },
        },
        orderBy: { createdAt: 'desc' },
      },
    },
  })

  if (!post) {
    notFound()
  }

  // 更新阅读次数
  await db.posts.update({
    where: { id: post.id },
    data: { views: { increment: 1 } },
  })

  return (
    <article>
      <header>
        <h1>{post.title}</h1>
        <div>
          <span>By {post.author.name}</span>
          <span>{post.createdAt.toLocaleDateString()}</span>
        </div>
        <div>
          {post.tags.map((tag) => (
            <span key={tag.id}>#{tag.name}</span>
          ))}
        </div>
      </header>

      <div dangerouslySetInnerHTML={{ __html: post.content }} />

      <section>
        <h2>Comments ({post.comments.length})</h2>
        {post.comments.map((comment) => (
          <div key={comment.id}>
            <p>
              {comment.author.name}: {comment.content}
            </p>
            <time>{comment.createdAt.toLocaleDateString()}</time>
          </div>
        ))}
      </section>
    </article>
  )
}

3. 用户仪表盘

// app/dashboard/page.js
import { db } from '@/lib/database'
import { auth } from '@/lib/auth'

export default async function Dashboard() {
  const session = await auth()

  if (!session) {
    return <div>Please log in</div>
  }

  // 获取用户数据
  const user = await db.users.findUnique({
    where: { id: session.user.id },
    include: {
      orders: {
        orderBy: { createdAt: 'desc' },
        take: 5,
      },
      profile: true,
    },
  })

  // 获取统计数据
  const stats = await db.orders.aggregate({
    where: { userId: session.user.id },
    _sum: { total: true },
    _count: true,
  })

  return (
    <div>
      <h1>Welcome, {user.name}</h1>

      <div>
        <h2>Statistics</h2>
        <p>Total Orders: {stats._count}</p>
        <p>Total Spent: ${stats._sum.total || 0}</p>
      </div>

      <div>
        <h2>Recent Orders</h2>
        {user.orders.map((order) => (
          <div key={order.id}>
            <p>Order #{order.id}</p>
            <p>Total: ${order.total}</p>
            <p>Status: {order.status}</p>
          </div>
        ))}
      </div>
    </div>
  )
}

安全最佳实践

1. 数据访问控制

// lib/auth.js
import { db } from './database'

export async function auth() {
  // 实现身份验证逻辑
  // 返回用户会话信息
}

export async function requireAuth() {
  const session = await auth()
  if (!session) {
    throw new Error('Unauthorized')
  }
  return session
}

export async function requireRole(role) {
  const session = await requireAuth()
  if (session.user.role !== role) {
    throw new Error('Insufficient permissions')
  }
  return session
}

2. 数据验证

// app/api/secure-data/route.js
import { z } from 'zod'
import { db } from '@/lib/database'
import { requireAuth } from '@/lib/auth'

const dataSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().min(18),
})

export async function POST(request) {
  try {
    // 身份验证
    const session = await requireAuth()

    // 数据验证
    const body = await request.json()
    const validatedData = dataSchema.parse(body)

    // 数据库操作
    const result = await db.users.create({
      data: {
        ...validatedData,
        createdBy: session.user.id,
      },
    })

    return NextResponse.json(result)
  } catch (error) {
    return NextResponse.json({ error: error.message }, { status: 400 })
  }
}

3. 环境变量管理

// lib/config.js
export const config = {
  database: {
    url: process.env.DATABASE_URL,
    // 其他数据库配置
  },
  auth: {
    secret: process.env.JWT_SECRET,
    // 其他认证配置
  },
  api: {
    key: process.env.API_SECRET_KEY,
    // 其他 API 配置
  },
}

// 验证必需的环境变量
const requiredEnvVars = ['DATABASE_URL', 'JWT_SECRET', 'API_SECRET_KEY']

for (const envVar of requiredEnvVars) {
  if (!process.env[envVar]) {
    throw new Error(`Missing required environment variable: ${envVar}`)
  }
}

总结

Server Component 直接访问后端服务的优势:

安全性

  • 代码在服务器端执行
  • 环境变量不会暴露给客户端
  • 数据库连接字符串安全
  • 敏感数据不会发送到客户端

性能

  • 减少 API 调用
  • 直接在服务器端处理数据
  • 更好的缓存控制
  • 减少客户端 JavaScript 包大小

开发体验

  • 更简洁的代码
  • 更好的类型安全
  • 更清晰的数据流
  • 更少的样板代码

最佳实践

  • 使用环境变量保护敏感信息
  • 实现适当的身份验证和授权
  • 验证和过滤数据
  • 使用类型安全的数据库客户端
  • 实现错误处理和日志记录