Next.js 14中如何用Next Auth正确保护Server Actions?
嘿,这个问题我刚上手Server Actions的时候也踩过一模一样的坑!确实,Next Auth的中间件只负责拦截页面路由和API路由的请求,但Server Actions是通过Next.js内部的RPC机制调用的,中间件不会直接管这些请求,所以才会出现你说的未登录用户也能调用敏感查询的情况。不过解决起来思路很明确——必须在Server Actions内部做服务器端的会话验证,而且可以通过封装来避免重复代码,下面给你具体的实现方案:
方案一:在核心数据库操作函数中添加认证校验
既然你所有的数据库操作都通过executeQuery来执行,那最直接的方式就是在这个函数开头加上会话验证。因为Server Actions是运行在服务器端的,你可以直接用Next Auth的getServerSession来获取当前会话,而不需要依赖客户端的useSession(客户端状态是不可信的,必须在服务器端校验)。
首先确保你已经在Next Auth的配置文件(一般是app/api/auth/[...nextauth]/route.js)中导出了authOptions,然后在actions/db.js中导入它并添加校验:
import { getServerSession } from "next-auth/next"; // 导入你的Next Auth配置 import { authOptions } from "../app/api/auth/[...nextauth]/route"; export async function executeQuery(query, values = []) { // 第一步:验证用户是否已登录 const session = await getServerSession(authOptions); if (!session) { // 抛出未授权错误,客户端调用时会捕获到这个错误 throw new Error("未授权访问,请先登录"); } // (可选)如果需要更细粒度的权限控制,比如只有管理员能查用户数据 // if (session.user.role !== "admin") { // throw new Error("权限不足,无法执行此操作"); // } // 原有的数据库操作逻辑 await connectDatabase(); const connection = await pool.getConnection(); try { const [results] = await connection.execute(query, values); return results; } finally { connection.release(); } }
这样一来,任何未登录的用户调用executeQuery都会直接抛出错误,没法执行数据库操作。
方案二:封装通用的认证校验函数(推荐)
如果你的Server Actions不止数据库操作,还有其他敏感逻辑,最好把认证逻辑封装成一个通用函数,避免在每个Action里重复写校验代码:
// actions/auth-utils.js import { getServerSession } from "next-auth/next"; import { authOptions } from "../app/api/auth/[...nextauth]/route"; export async function requireAuth() { const session = await getServerSession(authOptions); if (!session) { throw new Error("Unauthorized"); } // 返回会话信息,方便后续做权限判断 return session; }
然后在需要保护的Server Actions里直接调用这个函数:
// actions/db.js import { requireAuth } from "./auth-utils"; export async function executeQuery(query, values = []) { // 先校验认证状态 const session = await requireAuth(); // (可选)权限校验 // if (!session.user.permissions.includes("read_users")) { // throw new Error("无权限读取用户数据"); // } // 原数据库逻辑... } // 其他敏感Action示例 export async function updateUserProfile(userId, data) { const session = await requireAuth(); // 确保用户只能修改自己的资料 if (session.user.id !== userId) { throw new Error("无法修改他人资料"); } // 执行更新操作... }
这种方式更灵活,也更容易维护,后续要调整认证逻辑只需要改requireAuth一个地方就行。
关键注意事项
- 永远不要依赖客户端的状态做校验:客户端的
useSession只能用来做UI展示,不能用来判断是否允许执行敏感操作——因为客户端状态可以被篡改,必须在服务器端用getServerSession做最终校验。 - 保持参数化查询:你现在用
?占位符的方式很好,一定要坚持,这是防止SQL注入的核心手段,即使做了认证也不能放松。 - 区分公开和受保护的操作:如果有些数据库查询是允许未登录用户调用的(比如首页的公开内容),可以把
executeQuery拆成两个函数:publicExecuteQuery(无校验)和protectedExecuteQuery(带校验),避免误保护。
关于API路由的疑问
你提到是不是要回到API路由——其实完全没必要,Server Actions的设计就是为了简化后端逻辑,只要做好服务器端的认证校验,它比API路由更方便。API路由的保护本质上也是在路由 handler 里做同样的getServerSession校验,和Server Actions的思路是一致的。
备注:内容来源于stack exchange,提问作者Benjamin Göller

