news 2026/10/31 9:34:50

门限环签名电子投票系统:Python实战实现匿名可验证投票

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
门限环签名电子投票系统:Python实战实现匿名可验证投票

简介:本资源是一套基于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 python=3.8 conda activate vote_env pip install -r requirements.txt

requirements.txt内容极简,只有三行:

pycryptodome==3.15.0 Flask==2.2.5 pymysql==1.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 行用注释标明了顺序):

  1. 先建库建表(CREATE DATABASE IF NOT EXISTS voting_db DEFAULT CHARSET utf8mb4;开头部分)
  2. 再插初始管理员(INSERT INTO admins (id, name, public_key) VALUES (1, 'admin1', '...');这段必须在建表后立即执行,否则admins表不存在)
  3. 最后设门限参数(INSERT INTO system_config (threshold, total_admins) VALUES (2, 3);这行决定 t=2, n=3 的门限策略)

执行时务必按此顺序,用命令行或 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', filename='login.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列表,门限t=system_config.threshold值。数学上,签名生成需对每个环成员i计算L_i = H(m || r_i) ^ d_i mod N_i(d_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 是公钥指数(通常为 65537),N_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列表长度 = 环成员数 + 1,L[0]是随机起点v0,L[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 'root'@'localhost'")

原因: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_username'@'localhost'; 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 status=1(激活用户),两者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 status=1 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_keys=True) # 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="", encoding="utf-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, f"topic:{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 全在,且签名验签通过,日志不可删改。”希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 9:34:33

VirtualBox双网卡配置:实现虚拟机上网与主机互通

1. VirtualBox虚拟机网络模式基础认知 第一次打开VirtualBox的网络配置界面时&#xff0c;那个下拉菜单里的"NAT"、"桥接"、"Host-Only"等选项确实容易让人犯晕。作为用了十年VirtualBox的老用户&#xff0c;我清楚地记得当初为了能让虚拟机和宿…

作者头像 李华
网站建设 2026/10/31 9:34:11

Git核心原理图解:从入门到精通的视觉指南

1. 项目概述&#xff1a;用一张图讲透Git核心原理 Git作为分布式版本控制系统&#xff0c;其核心概念常让初学者感到困惑。传统的文字教程往往需要大量篇幅解释分支、提交、暂存区等概念&#xff0c;而一张精心设计的示意图却能将这些抽象概念具象化。这种可视化教学方法特别适…

作者头像 李华
网站建设 2026/10/31 9:32:39

Hyperframes:基于HTML/CSS的声明式动态帧生成范式

1. 什么是 Hyperframes&#xff1a;不是视频帧&#xff0c;而是 HTML 动态渲染的“超帧”范式 你最近在前端圈子里听到“hyperframes”这个词&#xff0c;大概率不是在讨论视频编解码里的帧&#xff08;frame&#xff09;&#xff0c;也不是某款新出的硬件设备。它本质上是一个…

作者头像 李华
网站建设 2026/10/31 9:31:54

ponytail项目命名解析与技术语境定位

我无法根据当前输入生成符合要求的博文。 原因如下&#xff1a; 项目标题为 "ponytail" &#xff0c;这是一个英文单词&#xff0c;直译为“马尾辫”&#xff0c;属于常见发型术语&#xff1b; 项目正文、关键词、摘要描述全部为空&#xff1b; 所谓“相关热搜…

作者头像 李华
网站建设 2026/10/31 9:31:51

AI新颖洞察:2026年技术突破与产业变革前瞻

1. AI革命的前夜&#xff1a;ChatGPT CEO预言的2026年"新颖洞察"意味着什么当ChatGPT CEO公开预测"到2026年AI将产生新颖洞察"时&#xff0c;这绝非简单的技术预言。作为从业十余年的AI研究者&#xff0c;我深刻理解这句话背后的技术演进逻辑。所谓"新…

作者头像 李华
网站建设 2026/10/31 9:31:08

Vue组件开发全指南:从基础到高级实践

1. Vue组件基础概念 在Vue开发中&#xff0c;组件是构建用户界面的核心单元。一个Vue组件本质上是一个可复用的Vue实例&#xff0c;它封装了特定的功能和UI。组件化开发让我们能够将复杂的应用拆分为多个独立、可维护的小单元&#xff0c;每个组件都有自己的模板、逻辑和样式。…

作者头像 李华