简介:本资源是一套基于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.txtrequirements.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 行用注释标明了顺序):
- 先建库建表(
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);这行决定 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表并重构签名消息:
| 字段 | 原类型 | 新类型 | 说明 |
|---|---|---|---|
message | VARCHAR(100) | TEXT | 存储 JSON,如{"position":"class_rep","candidate":"Alice","timestamp":1717023456} |
topic_id | INT | INT | 新增字段,关联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 全在,且签名验签通过,日志不可删改。”希望帮到你。
本文还有配套的精品资源,点击获取