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

Next.js 14:为无认证访客分配唯一ID 限制单次游戏参与方案咨询

无认证访客唯一标识方案(Next.js 14)

针对餐厅转盘抽奖的防重复参与需求,结合你遇到的问题,推荐以下几个务实的方案,按可靠性和实现成本排序:

方案一:服务端主导的Cookie+Redis持久化校验(首选)

核心思路是把唯一标识的控制权放在服务端,客户端仅做辅助存储,彻底避免前端篡改或隐私模式绕过的问题。

实现步骤

  1. 生成并存储访客ID:
    • 首次访问时,服务端生成UUID作为访客ID,设置带有HttpOnly、Secure、SameSite=Strict属性的Cookie(有效期可设为30天,根据活动周期调整)。
    • 同时将ID返回给前端,存在LocalStorage中作为备用(应对隐私模式下Cookie丢失的同会话场景)。
  2. 抽奖接口校验:
    • 用户请求抽奖时,优先读取Cookie中的访客ID,若不存在则尝试读取前端传入的LocalStorage中的ID。
    • 用Redis快速查询该ID是否已有抽奖记录,有则拒绝,无则执行抽奖逻辑并标记记录。

Next.js 14代码示例

访客ID生成API(App Router)

// app/api/visitor-id/route.ts
import { v4 as uuidv4 } from 'uuid';
import { cookies } from 'next/headers';
import { redis } from '@/lib/redis'; // 需提前配置Redis连接

export async function GET() {
  const cookieStore = cookies();
  let visitorId = cookieStore.get('restaurant_lottery_id')?.value;

  if (!visitorId) {
    visitorId = uuidv4();
    // 设置安全Cookie,生产环境开启Secure
    cookieStore.set('restaurant_lottery_id', visitorId, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 30 * 24 * 60 * 60, // 30天有效期
      path: '/',
    });
  }

  return Response.json({ visitorId });
}

抽奖接口

// app/api/lottery/route.ts
import { cookies } from 'next/headers';
import { redis } from '@/lib/redis';

export async function POST() {
  const cookieStore = cookies();
  const visitorId = cookieStore.get('restaurant_lottery_id')?.value;

  if (!visitorId) {
    return Response.json(
      { success: false, message: '请刷新页面后重试' },
      { status: 403 }
    );
  }

  // 检查是否已参与过抽奖
  const hasJoined = await redis.get(`lottery:participant:${visitorId}`);
  if (hasJoined) {
    return Response.json(
      { success: false, message: '您已参与过本次抽奖' },
      { status: 403 }
    );
  }

  // 执行抽奖逻辑(示例:随机生成奖品)
  const prizes = ['50元优惠券', '免费小吃', '谢谢参与'];
  const selectedPrize = prizes[Math.floor(Math.random() * prizes.length)];

  // 标记已参与,有效期和Cookie一致
  await redis.set(`lottery:participant:${visitorId}`, '1', {
    EX: 30 * 24 * 60 * 60,
  });

  return Response.json({ success: true, prize: selectedPrize });
}

前端客户端组件

// app/components/LotteryButton.tsx
'use client';

import { useEffect, useState } from 'react';

export default function LotteryButton() {
  const [visitorId, setVisitorId] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    async function initVisitorId() {
      // 先从LocalStorage读取备用ID
      const storedId = localStorage.getItem('restaurant_lottery_id');
      if (storedId) {
        setVisitorId(storedId);
        return;
      }
      // 无备用则请求服务端生成
      const res = await fetch('/api/visitor-id');
      const data = await res.json();
      setVisitorId(data.visitorId);
      localStorage.setItem('restaurant_lottery_id', data.visitorId);
    }
    initVisitorId();
  }, []);

  const handleLottery = async () => {
    if (!visitorId || isLoading) return;
    setIsLoading(true);
    try {
      const res = await fetch('/api/lottery', { method: 'POST' });
      const data = await res.json();
      alert(data.success ? `恭喜您获得:${data.prize}` : data.message);
    } catch (err) {
      alert('抽奖失败,请稍后重试');
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <button
      onClick={handleLottery}
      disabled={!visitorId || isLoading}
      style={{ padding: '10px 20px', fontSize: '16px' }}
    >
      {isLoading ? '抽奖中...' : '点击参与抽奖'}
    </button>
  );
}

优缺点

  • 优点:服务端控制核心校验逻辑,HttpOnly Cookie难以被篡改;Redis性能优异,适配高频抽奖场景;结合LocalStorage可减少隐私模式下的重复参与。
  • 缺点:若用户刻意清除Cookie并使用隐私模式,可能获得新ID,但这类极端操作的用户占比极低,完全满足餐厅活动的防刷需求。

方案二:轻量设备指纹+服务端绑定(补充增强)

如果想进一步降低方案一的漏防概率,可以自己实现轻量设备指纹,结合服务端的访客ID记录。无需依赖第三方付费服务,仅收集稳定的客户端信息生成标识。

轻量指纹生成示例

// 前端生成轻量指纹
async function generateLightFingerprint(): Promise<string> {
  const stableComponents = [
    window.screen.width + 'x' + window.screen.height,
    Intl.DateTimeFormat().resolvedOptions().timeZone,
    navigator.language,
    navigator.hardwareConcurrency || 'unknown',
    navigator.platform,
  ];
  // 使用SHA-256生成哈希值(更稳定)
  const encoder = new TextEncoder();
  const data = encoder.encode(stableComponents.join('|'));
  const hashBuffer = await crypto.subtle.digest('SHA-256', data);
  return Array.from(new Uint8Array(hashBuffer))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
}

增强逻辑

在访客ID生成时,将指纹与ID绑定存储到Redis;当用户清除Cookie后,前端上传指纹,服务端查询是否有绑定的访客ID,若有则拒绝抽奖。此方法能覆盖部分刻意清除Cookie的用户,但存在极低的碰撞概率(不同设备生成相同指纹)。

方案三:邮箱/手机号一次性验证(绝对可靠)

如果活动允许增加用户操作步骤,可要求用户输入邮箱或手机号,通过验证码验证身份,服务端记录已验证的账号。此方法能100%避免重复参与,但会降低用户参与意愿,适合高价值奖品的抽奖活动。


内容的提问来源于stack exchange,提问作者Hedi Fourati

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 06:44:51