You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 22:04:55