
简介本资源是一套基于Python实现的门限环签名电子投票系统毕设项目面向计算机类专业本科生、研究生及初学者解决传统电子投票中身份匿名性、防篡改与可验证性等核心安全问题。压缩包共104个文件含24个核心Python源码涵盖密钥生成、签名构造、投票验证等模块、37个pyc编译文件、17张界面与流程图PNG、1个SQL数据库脚本及README.md说明文档整体仅331KB轻量易部署。已有173人下载学习项目经实际运行测试与毕业答辩验证功能完整稳定评审平均分达96分。读者可直接复现完整投票流程深入理解门限密码学与环签名在隐私保护场景下的工程落地亦可基于现有结构快速扩展多中心投票、审计日志或Web前端集成等功能。1. 门限环签名电子投票系统不是“加密炫技”而是解决“谁投了票但又不能查到是谁”这个真实痛点的毕业设计落地项目你有没有遇到过这种场景班级要匿名选班委但班长说“我来收票、我来唱票”结果有人质疑“你是不是偷偷改了票”或者课程设计要求做“可验证、不可追踪、抗合谋”的投票系统一搜全是理论论文代码要么缺数据库、要么跑不起来、要么注释为零这个毕业设计资源就是冲着这些血泪现场来的——它用 Python 实现了一套完整可用的门限环签名Threshold Ring Signature电子投票系统不是 Demo不是截图是答辩拿了 96 分、所有模块用户注册、密钥生成、投票提交、计票验证全部跑通的真实工程。核心价值在于投票者身份对管理员完全隐藏环签名但任意 t 个管理员联合才能解密并验证签名有效性门限机制既防篡改又保匿名还规避了中心化 CA 的单点风险。适合计算机类专业学生直接当毕设交、老师拿来当课设范例、企业内训做密码学实践案例。别被“门限”“环签名”吓住——它没用 OpenSSL 黑匣子调用所有密码逻辑都摊开在ring_signature.py和threshold.py里连模幂运算都手写新手照 README 步骤装依赖、建库、跑main.py就能看见 login.jpeg 那张真实的登录界面弹出来。2. 从零跑通环境搭建、数据库初始化与核心模块启动流程2.1 环境准备Python 版本、依赖包与开发工具链确认这个项目对 Python 版本有明确要求必须使用 Python 3.8.x不是 3.9也不是 3.7。原因很实在——pycryptodome库在 3.9 中对某些 RSA 密钥生成接口做了非兼容性调整而本项目的keyGen.png流程图里明确标注了密钥长度为 2048 位且依赖Crypto.PublicKey.RSA的旧版参数签名方式。如果你用python --version查出来是 3.10请立刻用 pyenv 或 conda 创建隔离环境# 推荐用 conda比 venv 更稳尤其涉及 crypto 库时 conda create -n vote_env python3.8 conda activate vote_env pip install -r requirements.txtrequirements.txt内容极简只有三行pycryptodome3.15.0 Flask2.2.5 pymysql1.1.0注意pycryptodome版本锁死在 3.15.0——这是作者实测唯一能稳定通过test_ring_signature.py中全部 12 个测试用例的版本。高版本会出现ValueError: RSA key format not supported低版本则Crypto.Signature.pkcs1_15.new()缺少can_sign()方法。Flask 2.2.5是为了兼容login.jpeg界面中使用的render_template_string动态渲染逻辑新版 Flask 默认禁用该函数。提示不要用pip install pycrypto这是已废弃的旧库和pycryptodome冲突。如果报ModuleNotFoundError: No module named Crypto先pip uninstall pycrypto再重装pycryptodome。2.2 数据库初始化SQL 文件执行顺序与表结构关键字段说明项目根目录下的voting_system.sql不是单个建库脚本而是分阶段执行的三段式 SQL作者在README.md第 4 行用注释标明了顺序先建库建表CREATE DATABASE IF NOT EXISTS voting_db DEFAULT CHARSET utf8mb4;开头部分再插初始管理员INSERT INTO admins (id, name, public_key) VALUES (1, admin1, ...);这段必须在建表后立即执行否则admins表不存在最后设门限参数INSERT INTO system_config (threshold, total_admins) VALUES (2, 3);这行决定 t2, n3 的门限策略执行时务必按此顺序用命令行或 DBeaver 执行mysql -u root -p voting_system.sql关键表结构需特别注意users表中public_key字段类型为TEXT不是VARCHAR(2048)——因为 RSA 公钥 Base64 编码后长度浮动超长会截断votes表的signature字段长度设为TEXT存储的是环签名序列化后的 JSON 字符串含多个成员公钥、随机数、签名值不是简单哈希system_config表只有一行数据threshold值直接影响threshold.py中verify_threshold_signature()函数的签名验证逻辑——若这里填错所有投票都会验签失败。2.3 启动主服务Flask 路由映射与前端静态资源路径校验项目入口是main.py运行命令为python main.py此时 Flask 默认监听http://127.0.0.1:5000。但注意所有前端图片login.jpeg,signin.jpeg等必须放在static/目录下否则render_template(login.html)中的img src{{ url_for(static, filenamelogin.jpeg) }}会 404。作者把图片文件名直接写进 HTML 模板没做动态路径配置所以你下载后若发现页面显示“图片加载失败”第一反应不是代码问题而是检查static/login.jpeg是否存在。路由设计非常清晰/→ 重定向到/login避免首页空白/login→ 渲染login.html提交 POST 到/auth/vote→ 投票页需 session 认证提交 POST 到/submit_vote/results→ 计票页仅管理员可访问通过session[role] admin判断注意/auth路由中对密码做了 SHA256 加盐哈希盐值硬编码在config.py的SALT voting2024不是明文比对。所以你在数据库users表里看到的password_hash是sha256(salt raw_password)不是原始密码。3. 门限环签名实现原理从数学定义到 Python 代码逐行拆解3.1 为什么选门限环签名对比普通数字签名与群签名的不可替代性普通 RSA 签名能证明“消息来自某私钥持有者”但暴露了签名者身份群签名Group Signature虽支持匿名但需要可信群管理员TA来撤销成员资格——这在投票场景中等于把“谁能投票”的权力交给单点违背去中心化初衷。而门限环签名完美避开这两坑环签名特性签名者从一个公开公钥集合如所有注册用户中“隐匿”自己验证者只能确认“签名来自该集合中的某人”无法定位具体是谁门限特性验证过程不依赖单个管理员而是要求至少t个管理员如 3 个中的任意 2 个协作提供部分签名缺一不可——这既防止单点腐败又避免因个别管理员离线导致系统瘫痪。本项目中环成员集 所有已注册用户的public_key列表门限tsystem_config.threshold值。数学上签名生成需对每个环成员i计算L_i H(m || r_i) ^ d_i mod N_id_i为私钥N_i为模数但实际代码做了工程简化只对真实投票者计算私钥操作其余成员用其公钥模拟“伪签名”大幅降低计算开销。这正是ring_signature.py中generate_ring_signature()函数的核心逻辑。3.2ring_signature.py关键函数解析generate_ring_signature()与verify_ring_signature()签名生成函数接收三个参数message投票内容如candidate_A、private_key投票者自己的 RSA 私钥、ring_public_keys环内所有用户的公钥列表含自己。代码主体如下def generate_ring_signature(message, private_key, ring_public_keys): # Step 1: 生成随机数 v0用于后续链式计算 v0 random.randint(1, private_key.n // 2) # Step 2: 对环中每个公钥计算伪签名 L_i H(m || v_{i-1}) ^ e_i mod N_i # 注意这里 e_i 是公钥指数通常为 65537N_i 是公钥模数 L [v0] for i, pk in enumerate(ring_public_keys): h int(hashlib.sha256((message str(L[-1])).encode()).hexdigest()[:16], 16) # 用公钥加密L_i h^e mod N L_i pow(h, pk.e, pk.n) L.append(L_i) # Step 3: 投票者用自己的私钥“闭合环”计算 s (v0 - h_k)^d mod N_k # 其中 k 是投票者在 ring_public_keys 中的索引 k ring_public_keys.index(private_key.publickey()) # 定位自己位置 h_k int(hashlib.sha256((message str(L[k])).encode()).hexdigest()[:16], 16) s pow(v0 - h_k, private_key.d, private_key.n) return {s: s, L: L[1:], ring_keys: [pk.export_key().decode() for pk in ring_public_keys]}这段代码的精妙在于L列表长度 环成员数 1L[0]是随机起点v0L[1:]是各成员“伪签名”最终返回的s是唯一含私钥运算的部分L[1:]全是公开可计算的验证者无需知道谁是真实签名者ring_keys存储公钥字符串供验证方重建环结构。验证函数verify_ring_signature(sig, message, ring_public_keys)则反向推导用s和L[-1]重新计算h_k再用h_k和L[k-1]验证是否满足pow(h_k, ring_public_keys[k].e, ring_public_keys[k].n) L[k]。只要环中任意一个等式成立即证明签名有效——而攻击者无法反推k值因为L是单向链式生成的。3.3threshold.py如何让 t 个管理员协作完成签名验证门限机制不作用于签名生成而作用于签名验证结果的最终确认。threshold.py中的verify_threshold_signature()函数逻辑如下def verify_threshold_signature(signature, message, ring_public_keys, admin_partial_sigs): admin_partial_sigs: list of dict like [{admin_id: 1, partial_sig: 12345}, ...] # Step 1: 检查 partial_sigs 数量是否 threshold if len(admin_partial_sigs) get_threshold(): # 从 database 读取 threshold return False # Step 2: 每个管理员用自己私钥对 signature[s] 做部分签名 # 这里简化为partial_sig s * admin_secret mod p实际用 Shamir 秘密共享 partial_values [ps[partial_sig] for ps in admin_partial_sigs] # Step 3: 用 Lagrange 插值重构完整 s reconstructed_s lagrange_interpolate(partial_values, get_threshold()) # Step 4: 用重构的 s 验证环签名 return verify_ring_signature({s: reconstructed_s, L: signature[L], ...}, message, ring_public_keys)关键点admin_partial_sigs不是管理员直接签名原始消息而是对signature[s]环签名核心值做部分签名lagrange_interpolate()实现了 Shamir 门限方案确保少于t个部分无法重构s最终验证仍调用verify_ring_signature()保证环签名语义不变。4. 避坑指南运行失败、验签失败、数据库连接异常的 5 条血泪经验4.1 现象启动main.py后报错sqlalchemy.exc.OperationalError: (pymysql.err.OperationalError) (1045, Access denied for user rootlocalhost)原因config.py中数据库连接配置写死为root用户但你的 MySQL 未设置 root 密码或密码不匹配。作者本地 MySQL 是免密登录但生产环境通常要求密码。解决打开config.py修改DB_CONFIG字典DB_CONFIG { host: 127.0.0.1, user: your_username, # 改为你创建的用户名 password: your_password, # 改为你设置的密码 database: voting_db, charset: utf8mb4 }然后用该用户登录 MySQL执行GRANT ALL PRIVILEGES ON voting_db.* TO your_usernamelocalhost; FLUSH PRIVILEGES;4.2 现象登录成功后点击“投票”页面跳转到/vote但显示Internal Server Error日志报KeyError: user_id原因/auth路由中session[user_id] user_id执行了但session对象未正确初始化或被覆盖。常见于 Flask 版本不匹配导致session未启用。解决检查main.py开头是否有app.secret_key voting_secret_key_2024必须有并在config.py中确认SECRET_KEY已设置。若用 gunicorn 部署还需在启动命令加--secret-key voting_secret_key_2024。4.3 现象投票提交后/results页面显示“暂无投票”但votes表里有数据且signature字段 JSON 格式正确原因/results路由中调用get_valid_votes()函数该函数内部对每条signature执行verify_ring_signature()但ring_public_keys传入的是空列表——因为get_all_public_keys()从数据库查users.public_key时public_key字段被 MySQL 自动截断建表时未设TEXT类型。解决进 MySQL 执行ALTER TABLE users MODIFY public_key TEXT;然后重新插入用户数据或用voting_system.sql重置库。4.4 现象管理员登录后/results显示“验证失败签名不匹配”但同一投票在本地测试脚本test_ring_signature.py中能通过原因ring_public_keys传入顺序不一致。环签名验证严格依赖公钥顺序而get_all_public_keys()返回的列表顺序由SELECT * FROM users ORDER BY id决定但generate_ring_signature()中构建环时用的是SELECT public_key FROM users WHERE status1激活用户两者ORDER BY子句不同导致顺序错乱。解决统一在get_all_public_keys()和签名生成处都加ORDER BY id ASC例如# 在 database.py 中修改 def get_all_public_keys(): cursor.execute(SELECT public_key FROM users WHERE status1 ORDER BY id ASC) return [RSA.import_key(pk[0]) for pk in cursor.fetchall()]4.5 现象keyGen.png图中显示密钥生成成功但users表里public_key字段为空原因register_user()函数中cursor.execute(INSERT INTO users (...) VALUES (...), (..., public_key_bytes))但public_key_bytes是bytes类型MySQLTEXT字段无法直接存入二进制。解决插入前将公钥转为 PEM 字符串public_key_pem public_key.export_key().decode() cursor.execute(INSERT INTO users (username, password_hash, public_key) VALUES (%s, %s, %s), (username, password_hash, public_key_pem))5. 进阶技巧如何把这套系统改成支持多议题投票、添加时间戳防重放、以及导出审计日志5.1 支持多议题投票修改votes表结构与签名消息构造逻辑当前系统只支持单议题如“选班长”message固定为候选人名。要支持“同时投班长、团支书、学习委员”需扩展votes表并重构签名消息字段原类型新类型说明messageVARCHAR(100)TEXT存储 JSON如{position:class_rep,candidate:Alice,timestamp:1717023456}topic_idINTINT新增字段关联topics表存议题名称、截止时间签名时message不再是纯字符串而是序列化后的字典# 在 submit_vote 路由中 vote_data { position: request.form[position], candidate: request.form[candidate], timestamp: int(time.time()), topic_id: int(request.form[topic_id]) } message json.dumps(vote_data, sort_keysTrue) # sort_keys 保证序列化顺序一致 signature generate_ring_signature(message, user_private_key, ring_keys)这样同一用户对不同议题的投票因topic_id和timestamp不同生成的签名必然不同杜绝重放攻击。5.2 添加时间戳防重放在验证环节强制检查时间窗口仅靠timestamp字段不够需在verify_ring_signature()前增加时间校验def verify_with_timestamp(signature, message, ring_public_keys): try: vote_data json.loads(message) now int(time.time()) # 允许 5 分钟误差防止客户端时间不准 if abs(now - vote_data[timestamp]) 300: return False, Timestamp out of range except (json.JSONDecodeError, KeyError, ValueError): return False, Invalid timestamp format return verify_ring_signature(signature, message, ring_public_keys), 并在/submit_vote路由中调用此函数而非直接调用原验证函数。5.3 导出审计日志生成 CSV 日志文件并记录关键操作在database.py中新增log_operation()函数记录所有敏感操作def log_operation(operation_type, user_id, details): timestamp datetime.now().strftime(%Y-%m-%d %H:%M:%S) with open(audit_log.csv, a, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([timestamp, operation_type, user_id, details])然后在关键位置调用/auth成功后log_operation(LOGIN, user_id)/submit_vote成功后log_operation(VOTE, user_id, ftopic:{topic_id})/results访问时log_operation(VIEW_RESULTS, session.get(user_id), admin)最终audit_log.csv可被 Excel 直接打开包含时间、操作、用户、详情四列满足毕设答辩对“可审计性”的硬性要求。从那以后我每次交付毕设代码都强制走一遍audit_log.csv生成流程——不是为了凑工作量而是当答辩老师问“怎么证明没人篡改投票结果”时我能直接打开这个 CSV指着第 17 行“2024-05-30 14:22:18,VOTE,105,topic:3”说“这是张三投给李四的票时间戳、用户 ID、议题 ID 全在且签名验签通过日志不可删改。”希望帮到你。本文还有配套的精品资源点击获取