Next.js 14未认证用户Server Action安全防护方案咨询
未认证用户Server Action防外部刷库安全方案
针对你在Vercel部署的Next.js 14项目(后端用Supabase),以下是几种可落地的安全措施,能有效阻止外部工具直接调用Server Action,同时允许网站未认证用户正常使用:
1. 集成CAPTCHA验证(最有效)
通过Cloudflare Turnstile或Google reCAPTCHA,在用户触发提交操作前完成人机验证,只有验证通过的请求才允许写入数据库。这类验证的令牌很难被脚本伪造,是防刷的核心手段。
客户端代码示例(以Cloudflare Turnstile为例)
'use client' import { useState } from 'react' import { Turnstile } from '@marsidev/react-turnstile' import { saveMessageAction } from '@/app/actions' export default function MessageForm() { const [inputText, setInputText] = useState('') const [loading, setLoading] = useState(false) const [captchaToken, setCaptchaToken] = useState('') const saveMessage = async () => { if (!inputText.trim() || !captchaToken) return setLoading(true) try { await saveMessageAction(inputText, captchaToken) setInputText('') } catch (err) { alert('提交失败,请重试') } finally { setLoading(false) setCaptchaToken('') } } return ( <div> <input type="text" value={inputText} onChange={(e) => setInputText(e.target.value)} disabled={loading} /> <Turnstile siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!} onSuccess={setCaptchaToken} refreshOnExpire={true} /> <button onClick={saveMessage} disabled={loading}> 提交消息 </button> </div> ) }
Server Action代码示例
'use server' import { db } from '@/lib/supabase' import { table_name } from '@/lib/schema' export async function saveMessageAction(message: string, captchaToken: string) { // 验证Turnstile令牌 const captchaRes = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ secret: process.env.TURNSTILE_SECRET_KEY!, response: captchaToken, }), }) const captchaData = await captchaRes.json() if (!captchaData.success) { throw new Error('人机验证失败') } // 基础内容校验 if (message.length < 1 || message.length > 200) { throw new Error('消息长度需在1-200字符之间') } // 写入数据库 await db.insert(table_name).values({ message }) }
2. 基于访客会话ID的限流
给每个未认证用户生成唯一的会话ID(存到本地存储或同站Cookie),结合会话ID+IP做双重限流,比单纯IP限流更难绕过——脚本无法批量生成有效的会话ID(必须先访问网站生成)。
客户端代码示例
'use client' import { useState } from 'react' import { saveMessageAction } from '@/app/actions' const getGuestSessionId = () => { let sessionId = localStorage.getItem('guest_session_id') if (!sessionId) { sessionId = crypto.randomUUID() localStorage.setItem('guest_session_id', sessionId) } return sessionId } export default function MessageForm() { const [inputText, setInputText] = useState('') const [loading, setLoading] = useState(false) const saveMessage = async () => { if (!inputText.trim()) return setLoading(true) try { await saveMessageAction(inputText, getGuestSessionId()) setInputText('') } catch (err) { alert('提交过于频繁,请稍后再试') } finally { setLoading(false) } } return ( <div> <input type="text" value={inputText} onChange={(e) => setInputText(e.target.value)} disabled={loading} /> <button onClick={saveMessage} disabled={loading}> 提交消息 </button> </div> ) }
Server Action代码示例(用Upstash Redis,Vercel可直接集成)
'use server' import { db } from '@/lib/supabase' import { table_name } from '@/lib/schema' import { Redis } from '@upstash/redis' const redis = new Redis({ url: process.env.UPSTASH_REDIS_URL!, token: process.env.UPSTASH_REDIS_TOKEN!, }) export async function saveMessageAction(message: string, sessionId: string) { // 限流规则:1分钟最多5次请求 const requestCount = await redis.incr(`guest:${sessionId}`) if (requestCount === 1) { await redis.expire(`guest:${sessionId}`, 60) } if (requestCount > 5) { throw new Error('请求过于频繁') } // 基础内容校验 if (message.length < 1 || message.length > 200) { throw new Error('消息长度不符合要求') } // 写入数据库 await db.insert(table_name).values({ message }) }
3. 利用Vercel Edge Middleware拦截非法请求
提前在Edge层过滤不符合条件的请求,减轻Server Action和数据库的压力,可结合域名校验、请求头检查等规则:
middleware.ts代码示例
import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' export function middleware(request: NextRequest) { // 判断是否为Server Action请求 const isServerAction = request.headers.has('next-action') || request.nextUrl.pathname.startsWith('/_next/data/') if (isServerAction) { // 校验请求来源域名(Vercel环境用x-forwarded-host) const allowedHost = process.env.NEXT_PUBLIC_APP_DOMAIN || 'your-domain.com' const requestHost = request.headers.get('x-forwarded-host') || request.headers.get('host') || '' if (!requestHost.includes(allowedHost)) { return NextResponse.json({ error: '非法请求' }, { status: 403 }) } // 基础User-Agent过滤(排除非浏览器请求) const userAgent = request.headers.get('user-agent') || '' if (!userAgent.includes('Mozilla/') && !userAgent.includes('Chrome/') && !userAgent.includes('Safari/')) { return NextResponse.json({ error: '非法请求' }, { status: 403 }) } } return NextResponse.next() } // 匹配所有页面请求,排除静态资源和API export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], }
4. 内容重复与语义校验
在写入数据库前,检查消息是否短时间内重复提交,同时过滤垃圾内容:
Server Action代码片段
// 检查是否1分钟内有相同消息提交 const recentMessage = await db .select() .from(table_name) .where(eq(table_name.message, message)) .where(gte(table_name.created_at, new Date(Date.now() - 60 * 1000))) .limit(1) if (recentMessage.length > 0) { throw new Error('请勿重复提交相同内容') } // 可选:调用Supabase内容审核API过滤违规内容 const moderationRes = await fetch(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/moderations`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': process.env.SUPABASE_SERVICE_ROLE_KEY!, 'Authorization': `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY!}`, }, body: JSON.stringify({ input: message }), }) const moderationData = await moderationRes.json() if (moderationData.flagged) { throw new Error('内容包含违规信息') }
内容的提问来源于stack exchange,提问作者MSmith
相关产品推荐
相关产品推荐

