
简介这是一套基于JSP的Web投票系统源代码面向学习Web开发的学生或初级开发者演示如何快速构建“学生给老师投票”的完整流程。系统运行后显示“欢迎给老师投票”界面表格清晰列出教师编号、姓名与实时得票数得票数以红色进度条直观呈现点击投票链接即可完成票数累加。代码按dao、db、tool等分包组织涵盖JDBC数据库操作、工具类封装和基础复用便于理解分层思想并快速改造。压缩包共24个文件约956KB主要包含4个Java源文件、4个编译后Class文件、3个JSP页面、2个Jar依赖库以及XML配置、图片素材等覆盖前端展示、后端逻辑与资源文件。已有1931人浏览学习可作为Web课程设计、实训项目或JSP入门参考学习投票功能实现之余还可进一步思考通过会话管理、IP限制等方式预防刷票提升系统的安全性。1. 投票系统源代码防刷票不是加个IP限制那么简单做投票系统的人十有八九是活动上线前才想起防刷票这回事。最常见的心态是先跑起来再说刷票的事后面补。结果活动第一天就被刷票机器人打穿前三名全是异常数据。我做过的投票项目里真正把防刷票做明白的靠的不是某一个“绝招”而是一套分层策略——从请求入口到业务逻辑每一层都卡一道。这套策略落到源代码里其实改动量不大但要把每个环节的阈值、存储、校验顺序想清楚否则就是给自己挖坑。这篇笔记就按我实际搭过的方案把投票系统源代码里防刷票的设计思路、核心代码和踩过的坑一次讲透适合正要自己做投票系统、或者被刷票搞到头大的开发者照着改。2. 防刷票的底层逻辑限制维度、阈值设置与策略组合2.1 刷票的本质绕过“一个人只能投一票”的假设任何投票系统的业务模型都建立在“一人一票”或“一人N票”的基础上。防刷票要做的就是尽可能让技术手段逼近这个假设。但HTTP请求天生无状态服务器无法直接知道对面是不是同一个人。刷票的本质就是伪装成不同的人或者伪造出大量合法请求。所以防刷票的思路不是找一个完美的“用户身份识别”方案——那个不存在——而是叠加多个弱信号让刷票成本高到不值得。常见的弱信号包括IP地址、User-Agent、Cookie、设备指纹canvas指纹、WebGL指纹、操作频率、投票时间分布。每个信号单独看都很容易被伪造但组合在一起能挡住90%以上的脚本刷票。我一般把防刷策略分成三层入口拦截层IP、UA、验证码、业务校验层频率、指纹、票数上限、事后分析层数据异常检测。入口层挡掉批量脚本业务层限制单设备/单人的投票数事后层用来发现漏网的异常模式。三层缺一不可——只做入口拦截绕过的人就全漏进来了只做业务校验正常的投票体验会被拖垮。2.2 阈值怎么定先定业务规则再定技术参数这里有个很容易颠倒的顺序很多人先写IP限制再想“每个IP能投几票”最后才回头想业务上到底该怎么限制。正确做法是先定业务规则——一个用户能投几票、投票周期多长、是否需要登录——然后技术参数跟着业务规则走。举例一个“每人每天可投3票”的活动技术上的阈值就应该是单IP每日上限通常放宽到业务上限的2-3倍因为一个公司/学校出口IP可能是多人共用、单设备指纹总上限严格等于业务上限、单IP小时级频率上限比如15分钟内不超过5次请求因为正常人不会连续快速投票。防刷参数设计里最忌讳的是“拍脑袋定数字”。我给出的经验值是所有技术阈值都要比业务上限宽松 30%~100%因为误杀正常用户比放走几个刷票更伤——投票活动的核心是参与度不是绝对公平。2.3 为什么必须引入Redis单机内存方案扛不住分布式早期做投票系统很多人用一个静态变量或者数据库表来计数。单机演示没问题一旦上线多实例部署时每个实例的内存计数互相独立刷票的人换个IP轮询后端节点就能绕过去。数据库计数可以共享状态但高频写入会把数据库拖垮而且投票接口的延迟会明显上升。防刷票场景里Redis几乎是标配。原因有三INCR/DECR 是原子操作天然支持并发计数EXPIRE 可以自动清理时间窗口不用手动删数据性能足够高单机10万 QPS 对投票系统绰绰有余。选 Redis 不是为了炫技是为了把“计数”这个高频操作的复杂度降下来。3. 投票系统源代码落地Redis计数、指纹生成与验证码接入3.1 整体架构一张表 两个Redis键就能跑起来做投票系统源代码最简可用的数据模型就三块候选对象表存被投的对象、投票记录明细表存每一笔投票、Redis键存防刷计数。完整生产环境还要加用户表但核心防刷逻辑不依赖用户表——匿名投票场景下刷票的人不会乖乖登录。投票接口的完整流程应该是先查Redis做频率限制 → 再做指纹和IP校验 → 校验通过后写数据库 → 写入成功后再更新Redis计数。注意顺序先限流再入库避免无效请求打到数据库。防刷校验放在数据库写入之前既是性能考虑也是逻辑正确性考虑。3.2 核心代码投票接口的防刷完整实现Python FastAPI Redis下面是我在一个实际项目中用过的投票接口核心代码。项目技术栈是 Python FastAPI Redis PostgreSQL业务规则是“每人每天可投3票”。import hashlib import json import time from typing import Optional import redis.asyncio as aioredis from fastapi import FastAPI, Request, HTTPException from pydantic import BaseModel app FastAPI() redis_client aioredis.from_url(redis://localhost:6379/0, decode_responsesTrue) # 业务规则常量 VOTE_DAILY_LIMIT 3 # 每人每天最多投3票 IP_HOURLY_LIMIT 10 # 单IP每小时最多10次投票请求 FINGERPRINT_DAILY_LIMIT 3 # 单设备指纹每天最多3票 REQUEST_INTERVAL_SECONDS 10 # 同一指纹两次投票的最小间隔秒 class VoteRequest(BaseModel): candidate_id: str device_fingerprint: Optional[str] None def generate_fingerprint(request: Request) - str: 生成设备指纹。 实际项目中指纹由前端采集 canvas/WebGL 信息后传入 这里做一层服务端兜底用 IP UA 关键头信息哈希生成。 raw f{request.client.host}|{request.headers.get(user-agent, )}|{request.headers.get(accept-language, )} return hashlib.sha256(raw.encode()).hexdigest() async def check_vote_eligibility(request: Request, payload: VoteRequest, fingerprint: str) - None: 防刷校验不通过直接抛异常调用方捕获后返回 403。 now int(time.time()) hour_window now // 3600 # 当前小时窗口 day_key fvote:day:{fingerprint}:{time.strftime(%Y%m%d)} hour_ip_key fvote:hour:ip:{request.client.host}:{hour_window} # 1. 校验当日总票数按设备指纹 daily_count await redis_client.get(day_key) if daily_count and int(daily_count) VOTE_DAILY_LIMIT: raise HTTPException(status_code403, detail今日投票次数已用完) # 2. 校验IP小时级频率 ip_hourly_count await redis_client.incr(hour_ip_key) if ip_hourly_count 1: await redis_client.expire(hour_ip_key, 3600) # 首次创建时设置过期 if ip_hourly_count IP_HOURLY_LIMIT: raise HTTPException(status_code403, detail请求过于频繁请稍后再试) # 3. 校验同一设备的投票间隔防连点 last_vote_key fvote:last:{fingerprint} last_vote_time await redis_client.get(last_vote_key) if last_vote_time and (now - int(last_vote_time)) REQUEST_INTERVAL_SECONDS: raise HTTPException(status_code403, detail投票操作过于频繁) async def record_vote(request: Request, payload: VoteRequest, fingerprint: str) - None: 投票入库 更新Redis计数。 # 入库逻辑INSERT INTO votes (candidate_id, fingerprint, ip, created_at) VALUES (...) # 用参数化查询避免SQL注入这里省略具体ORM代码 # 更新当日计数在入库成功后再更新Redis day_key fvote:day:{fingerprint}:{time.strftime(%Y%m%d)} await redis_client.incr(day_key) await redis_client.expire(day_key, 86400*2) # 多保留1天防时区偏差 # 记录本次投票时间 await redis_client.set(fvote:last:{fingerprint}, str(int(time.time())), exREQUEST_INTERVAL_SECONDS * 2) app.post(/api/vote) async def vote(payload: VoteRequest, request: Request): # 生产环境建议用请求体里的指纹这里用服务端生成的做兜底 fingerprint payload.device_fingerprint or generate_fingerprint(request) # 先做防刷校验校验不通过不触碰数据库 await check_vote_eligibility(request, payload, fingerprint) # 入库 更新计数 await record_vote(request, payload, fingerprint) return {code: 0, message: 投票成功}逻辑说明整个防刷校验被拆成三个独立步骤。第一步用vote:day:{指纹}:{日期}这个Key来限制当天总票数指纹由前端采集设备信息生成后面细说。第二步用vote:hour:ip:{IP}:{小时窗口}限制单IP的每小时请求次数这个Key用INCR自增首次创建时设置3600秒过期利用Redis原子性避免并发下计数错乱。第三步校验同一指纹两次投票的最小间隔防止连点器脚本。参数说明里最需要关注的三个值VOTE_DAILY_LIMIT3对应业务规则“每人每天3票”IP_HOURLY_LIMIT10是业务上限的3倍多给公司/学校共用IP留了余量REQUEST_INTERVAL_SECONDS10是防连点的最小间隔比正常人手速略低但足够挡住脚本。3.3 设备指纹前端采集 后端校验的组合做法纯后端生成指纹IP UA挡不住同网段刷票的人——同一个办公室出口IP一样UA也一样。正确做法是前端采集更细粒度的设备特征传到后端做哈希。前端采集的典型信息包括canvas 指纹画一张图不同GPU/浏览器渲染结果不同、WebGL 渲染器信息、屏幕分辨率、时区、系统语言、字体列表。这些信息拼在一起做哈希重复率极低。但注意这些信息也可能被模拟所以设备指纹从来不是银弹只是把刷票成本从“改IP”提升到“模拟整个浏览器环境”。我一般用fingerprintjs2这类库来采集但不会完全信任它返回的 visitorId——我会把它作为一部分再叠加服务端生成的指纹一起做哈希。前后端各出一半谁也骗不了谁。3.4 验证码只在风险请求时弹出不要每次都拦很多人做投票系统一上来就给所有投票加图形验证码。体验差不说打码平台几毛钱一千次毫无意义。验证码的正确使用方式是作为“风险触发后的二次验证”——正常用户全程无感只有被判定为可疑的请求才需要过验证码。实现方式在防刷校验通过后、入库之前加一个风险判定。如果当前请求的IP命中黑名单、或者频率异常返回一个need_captcha标志前端弹出滑块或点选验证码验证通过后带上 token 重新请求投票接口。这个 token 用 Redis 存5分钟过期一次性使用。这样既不伤正常用户体验又能把真正的高危请求挡在门外。4. 防刷参数的必调清单阈值、冷却时间与IP策略4.1 三个核心阈值参数这组参数是我在不同项目里反复调出来的可以直接抄走用。完整的参数表如下参数推荐初始值调整方向触发条件单指纹每日票数上限业务上限 × 1.0严格等于业务规则票数达到即拒绝单IP每小时请求上限业务上限 × 3~5活动越热门越放宽超过即拒绝单IP每日票数上限业务上限 × 5~8企业/校园网场景调高超过即拒绝同指纹投票最小间隔10~30 秒活动热度越高间隔越短间隔不足即拒绝验证码触发频率单IP小时请求 5次阈值越低越严格触发后弹验证码调参的核心逻辑先用最严参数跑小流量测试观察正常用户的投票数据分布。如果某个时间段的失败率超过5%说明参数太严得放宽如果刷票数据依然明显再逐步收紧。切忌一次性把参数调到极端——你会在误杀正常用户和漏放刷票之间反复横跳最后分不清数据是好是坏。4.2 IP策略公网IP、内网IP和CDN回源IP要分开处理IP限制最大的坑不是刷票的人而是正经的网络环境。公司、学校、商场通常整个区域共用一个出口IP如果拿单IP限制来卡几百个正常用户合起来就触发了限流导致全员投不了票。我的做法是前置一层IP分类——把请求IP分成三类。第一类是IDC机房的IP段阿里云、腾讯云等这类IP出现在投票请求里大概率是刷票脚本正常用户不会从云服务器投票直接掐掉。第二类是已知的企业/学校出口IP段这类IP放宽每小时上限甚至不做小时级限制只做每日总量。第三类是普通住宅IP按标准策略执行。IP分类表需要定期更新云厂商的IP段变化不快但也不是一成不变。我一般是每月拉一次各云厂商的IP段数据导入Redis里的黑名单。这个动作不复杂但能有效挡住最懒的一批刷票脚本——他们几乎不会伪装IP段。4.3 冷却时间的三种粒度秒级、分钟级和小时级冷却时间是防刷里最容易被低估的一块。很多人只做“两次投票的最小间隔”但间隔挡不住“每隔30秒投一次连续投2小时”的慢速脚本。所以冷却时间要分三种粒度叠加秒级冷却同一指纹两次投票至少间隔10秒防连点。分钟级冷却同一IP在5分钟内最多投3次防短时burst。小时级冷却同一指纹每小时最多投2票如果业务允许防长时间慢速刷。这个三层冷却的实现就是在Redis里加三个带不同过期时间的Key过期时间分别为10秒、300秒、3600秒。代码逻辑和前面check_vote_eligibility里的第三步完全一样复制改造即可。5. 防刷票常见问题与避坑从IP误伤到打码平台的攻防5.1 现象活动上线首日大量正常用户投票失败报“请求过于频繁”原因我把单IP小时级上限设成了10次结果一个学校几千人共用两个出口IP半小时就触顶了。这不是刷票是正常使用。解决加IP分类逻辑把已知的企业/学校IP段单独建一个Redis Set命中这个Set的请求跳过小时级限制只做每日总量限制。判断是否是“已知IP段”的方法很简单——活动预热期记录每天投票请求的IP分布把稳定贡献大量请求的IP段录入白名单。这个坑让我长记性任何IP限制都不能一视同仁先分类再限流。5.2 现象Redis里计数归零了但用户仍然提示“今日票数已用完”原因Redis Key 的过期时间设错了。我用expire(day_key, 86400)设置24小时过期但业务规则是“自然日限制”——用户凌晨0点投票Redis Key 如果是在昨天23点创建的今天0点一过还能用但到23点就过期了。用户晚上再投票时Key 已消失但数据库里的投票记录还在于是 Redis 放行数据库写入时唯一索引冲突报错提示“已用完”。解决把Key的过期时间从24小时改成48小时并且在生成Key时用日期字符串如vote:day:{指纹}:20250601而不是相对过期时间。这样即使过期时间有偏差Key 也会在日期切换后自然失效不会出现“计数还在但规则已换天”的错位。5.3 现象刷票数据还是进来了而且全部来自住宅IP单IP只投了1票原因这是最头疼的情况——刷票方用了代理池或者秒拨IP每个IP只投1票就换IP限制完全失效。Redis里的频率计数每个IP都只有1次看起来毫无异常。解决IP和指纹都不再是有效维度了这时候要换个思路。我从数据库里找规律发现这批异常的投票记录指纹都是同一台设备生成的canvas信息完全一致只是IP不同。原因找到了——防刷策略只做了IP维度没有把设备指纹作为同等重要的维度。于是我把指纹的优先级提到IP之前单指纹每日票数必须严格执行且指纹生成时不再依赖IP避免同设备换IP后指纹也变。另外加了最笨但最有效的兜底同一指纹的投票时间间隔做小样本分析正常用户的投票间隔呈正态分布刷票脚本则呈均匀间隔或整点间隔。5.4 现象验证码被绕过打码平台识别率接近100%原因滑块验证码和图形验证码在打码平台面前基本形同虚设。图形验证码用OCR滑块验证码用轨迹模拟识别率轻松上90%。这不是代码问题是验证码选型的问题。解决换成行为验证码比如点选文字、拖拽拼图并且增加“验证码一次性 短时过期”的逻辑——同一个验证码token只能用一次5分钟过期前端拿到token后不能重复提交。另外在服务端记录验证码的生成和校验耗时如果验证码在0.5秒内被完成直接判定为机器拒绝通过。正常人点选验证码至少需要2~3秒。这个0.5秒的判断标准是最实用的防打码手段。5.5 现象活动结束后人工复核发现Top10里有3个异常账号原因实时防刷拦截了一部分但漏掉的还是漏了。回顾后发现后置的风控分析没有跟上——没有定时的异常检测任务去扫描投票数据。解决加一个每天凌晨跑的定时任务对投票数据做统计分析。主要看三个指标单小时票数分布是否均匀真实投票会有明显的白天高峰和夜间低谷、单个候选对象的得票时间间隔是否规律刷票的间隔几乎相同自然投票的间隔是随机的、投票IP的地理位置分布是否集中正常投票会分散在活动覆盖区域。任何一个指标异常就把相关投票记录标记为“待人工审核”审核通过才计入最终结果。这个定时任务用简单的SQL Python脚本就能做不要搞复杂模型先跑规则有效再上模型。6. 事后校验比事前拦截更可靠投票数据风控的3个花招防刷票做到位不是靠拦截而是靠“即使刷了也能被查出来”。我最后的习惯做法是在活动结束后做一轮数据清洗用下面三个手段把漏网的异常数据揪出来。第一个方法是得票时间间隔分析。把每个候选对象的所有投票时间取出来按时间排序计算相邻两票的时间间隔。正常用户投票是随机的间隔分布会像噪声一样没有规律。刷票脚本为了快速完成任务间隔几乎恒定在某个固定秒数比如每秒1票或者每5秒1票。我用Python的numpy.std()算一下间隔标准差标准差小于0.5秒的候选对象基本可以判定为脚本刷票。这个逻辑实现成本极低但准确率惊人地高。第二个方法是投票来源集中度分析。把票数最高和最低的候选对象拉出来对比它们的IP分布和指纹分布。正常高票的IP分布会呈现“长尾”——大量IP各投1~2票少数IP投多票。刷票高票的IP分布要么是极端集中一个IP几百票要么是极端分散上千个IP每个1票。极端分散的情况很迷惑人但如果配合看投票时间你会发现这些“不同IP”的投票时间是连续且均匀的——人在睡觉票在增长这就是异常。第三个方法也是我用了很多次的“土办法”人工打电话回访。取排名前20的高票用户如果能关联到手机号随机抽5个打过去问是否参与过投票。如果5个里有1个说“没投过”或者“投过但不是这个号码”基本可以认定数据有水分。这个方法朴实但有效——我见过一个案例抽访5个人4个人表示没投过那个活动的最终排名全改了。做防刷票这几年我最深的体会是技术手段做得再严密也挡不住所有刷票但数据清洗能兜住最后的底。投票系统的公平性其实是靠“事前限制 事后清洗”两道关卡守住的。每个投票项目上线前我都会把这三个清洗脚本跑一遍确认基准数据正常才放开投票。这个习惯帮我避免了好几次活动结束后排名被质疑的翻车现场希望帮到你。本文还有配套的精品资源点击获取