用Python实现注册登录:文本文件存储与密码哈希实战

发布时间:2026/10/12 21:15:55
用Python实现注册登录:文本文件存储与密码哈希实战 最近帮一个刚学编程的朋友看作业需求就一句话实现注册、登录功能。用户名、密码存储在 user_g.txt 文件中。看完我笑了越是这种看着简单的小需求做起来越容易翻车。文件存用户信息听起来谁都会但真要交付一份能跑、能改、别人能看懂的代码里面涉及的细节比想象中多得多文件格式怎么定、密码要不要加密、重复注册怎么判断、读文件时编码和换行符怎么处理甚至还要考虑多人同时写文件会不会互相覆盖。这篇文章就把我完整实现这套注册登录功能的思路复盘一遍。讲解用 Python从存储设计、注册流程、登录流程到踩坑记录和后续扩展一步一步拆开说。适合刚学完基础语法、想拿小项目练手的初学者也适合做课程设计时直接参考。代码我会给出完整版本你照着敲一遍再改一改基本就能变成自己的东西。1. 先别急着写代码这个需求的定位和存储选型不管你拿到什么需求第一反应不应该是打开编辑器而是先想清楚这个需求到底在考什么、边界在哪里。注册登录是几乎所有系统都有的基础功能但不同的存储方式实现难度和适用场景完全不一样。1.1 文本文件存储到底在解决什么问题用 user_g.txt 这种文本文件存用户名和密码本质上是把一个用户表搬到了文件里。数据库里一张 users 表字段是 id、username、password_hash、created_at而文本文件里的每一行就相当于这张表里的一行记录。主题考察的是你能不能把增、查这两个操作用文件读写的方式实现出来。注册就是增——往文件里追加一条记录登录就是查——遍历文件找到对应用户名再比对密码。所以这个需求的核心并不是文件本身而是围绕文件读写做一层简单的数据访问封装。后面你会发现只要把这层封装做得规整将来从文件切换到数据库代价会小很多。1.2 为什么不是数据库也不是内存我在帮朋友梳理方案的时候他第一反应是想用 SQLite被我拦住了。原因很简单这个需求的难度等级对应的学习阶段大概率还没到数据库。用一种技术不是因为它更高级而是因为它更适合当下的目标。数据库的优势是查询、索引、并发控制但代价是引入额外的概念建表、连接、游标、事务。而文本文件的优势是零依赖、直观可见打开文件就能看到所有用户数据对初学者建立数据到底存在哪、长什么样的认知非常有帮助。内存存储比如一个全局字典虽然代码更简单但程序一退出数据就没了这就违背了存储的本意。文件刚好卡在中间既持久化又看得见还能练到文件读写这个必备技能。所以在选型之前先问自己一个问题这个功能的生命周期是多久数据量多大要不要并发如果是教学练习或者体量很小的内部工具文本文件完全够用。等数据量上来、查询变复杂了再迁数据库也不迟。1.3 这份需求里真正考核的点根据我给不少人批改同类作业的经验老师或面试官看这份代码关注的通常不是功能本身而是下面这几件事文件读取是否健壮文件不存在时会不会崩有没有处理空行密码是否明文存储如果直接用户名,密码写进文件基本一票否决。重复用户名检查是否有效注册前是否真的读了文件代码结构是否分层读写逻辑和业务逻辑是不是揉在一起了编码是否统一中文字符会不会乱码想通这几点再动手写代码思路就会清晰很多。下面我从头开始一步步把整个实现搭出来。2. user_g.txt 的格式设计改一次文件格式等于重写一遍逻辑存储格式是整个项目的地基。格式一旦定下来后面所有读写代码都要围绕它写。如果一开始随便拿个简单格式顶上后面想加点字段牵一发动全身。所以这部分我建议认真看。2.1 三种存储格式对比谁更抗折腾常见的文本存储格式有下面几种我也都试过格式方案示例优点缺点空格分隔alice e10adc3949ba59abbe56e057f20f883e人眼可读写起来最快用户名或内容一旦包含空格就全乱逗号分隔alice,salt值,哈希值简单直观兼容性好字段内容里不能出现逗号需要约定JSON 每行一个{user:alice,salt:...,hash:...}结构清晰可扩展对初学者偏复杂手动编辑易出错CSV 标准库用csv.writer写入自动处理引号和转义多一层抽象反而不利于理解原理我最推荐的是逗号分隔定稿格式为用户名,盐值,密码哈希一行一个用户。原因有三个第一是足够简单split(,)一下就拆开了第二是字段顺序固定读的时候按数组下标取就行第三是以后想加字段比如注册时间、登录失败次数在行尾追加一个逗号字段就能兼容老数据。2.2 定稿的存储结构与字段说明我最终采用的每一行格式如下alice,5f4dcc3b5aa765d61d8327deb882cf99,e10adc3949ba59abbe56e057f20f883e三个字段依次是用户名登录时用来定位记录的键要求唯一。盐值一段随机字符串每个用户都不一样用于防止哈希碰撞和彩虹表攻击。密码哈希将盐值和密码拼接后做 SHA-256 得到的结果绝不存明文密码。这里需要解释一下为什么要有盐值。如果只做sha256(密码)那么两个密码相同的用户哈希值完全一样而且攻击者可以用一份预计算的常见密码哈希表反查。加上每个用户独有的盐值之后即使密码都是123456因为盐值不同最终哈希也完全不同安全性提升一大截。对于教学项目能做到这一步已经超过了绝大多数同类作业。2.3 读写模块先抽象出来read_users / write_user动手写注册登录之前我强烈建议你先写一个独立的文件读写模块。把读取全部用户和追加一个用户封装成函数后面的业务代码只调用函数不直接碰文件。这样改格式的时候只需要改这一个地方。读取函数如下import hashlib import secrets import os USER_FILE user_g.txt def read_users(filepathUSER_FILE): 读取用户文件返回 {用户名: (盐值, 密码哈希)} 的字典。 users {} try: with open(filepath, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue parts line.split(,) if len(parts) ! 3: continue username, salt, hashed parts users[username] (salt, hashed) except FileNotFoundError: # 文件不存在时返回空字典而不是直接报错 pass return users几个细节值得说明第一encodingutf-8一定要写。如果不指定在中文 Windows 环境下会用 GBK 读取万一文件是 UTF-8 保存的中文用户名就会乱码。第二line.strip()是为了去掉行尾的\n。如果不去掉读出来的密码哈希末尾会莫名其妙多个回车导致登录永远比对失败。第三if len(parts) ! 3: continue是一个防御性判断。万一文件里混入一行格式错误的数据程序直接跳过而不是崩溃。文件数据源不可控这种防御习惯越早养成越好。追加写入就简单多了def append_user(username, salt, hashed, filepathUSER_FILE): with open(filepath, a, encodingutf-8) as f: f.write(f{username},{salt},{hashed}\n)这里用a追加模式新用户直接落在文件末尾不需要重写整个文件。这个设计对注册操作来说是最省的。3. 注册功能落地校验、加密、写文件一个都不能少注册是用户数据的入口入口没把好关后面登录再怎么写都是白搭。一个合格的注册流程至少要包含输入校验、密码加密、重复检查、写入文件四个环节。下面按顺序拆解。3.1 用户名规则我要拦截哪些脏数据用户名直接决定了文件里记录的唯一性和可读性所以第一步就是校验。我定的规则比较常规用户名长度 3 到 20 个字符太少没意义太长影响文件阅读。用户名不能包含逗号因为逗号是字段分隔符。用户名不能重复这个后面单独做检查。密码长度至少 6 位太短的项目直接拒绝别给用户留隐患。校验代码def validate_username(username): if len(username) 3 or len(username) 20: return False, 用户名长度需在3到20个字符之间 if , in username: return False, 用户名不能包含逗号 return True, 这里限死不能包含逗号不是因为我保守是真的会出问题。试想一下如果用户名里带逗号比如alice,admin写入文件后这一行会变成alice,admin,盐值,哈希读取时按逗号一拆字段数量从 3 变成 4直接被当成脏数据跳过用户就莫名其妙消失了。这种问题排查起来非常头疼所以入口处直接封死。3.2 密码不能明文存加盐哈希的正确姿势密码这一环教学项目里最常见的错误就是把密码原样写进文件。作为练习可以理解但我还是建议养成好习惯密码经过加盐哈希之后才落盘。哈希函数我用 Python 标准库的 hashlib盐值用 secrets 模块生成。secrets是专门用来生成密码学安全随机数的模块比random更适合安全场景。代码如下def hash_password(password, salt): return hashlib.sha256((salt password).encode(utf-8)).hexdigest()顺序上我习惯把盐值放在密码前面拼salt password。具体顺序其实不影响安全性只要约定统一注册和登录用同一套逻辑就行。还有一点encode(utf-8)不能漏因为 hashlib 接收的是字节串不能直接处理字符串。为什么不用 MD5MD5 已经是公认的不安全算法碰撞攻击成本低到离谱。SHA-256 虽然不是唯一的正确答案但作为入门足够而且标准库自带的实现连第三方库都不用装。再强调一遍真正的生产系统一般会选 bcrypt、scrypt 或 Argon2 这类专门为密码设计的慢哈希算法而不是通用的 SHA-256。原因是通用哈希算得太快攻击者可以每秒尝试海量密码。教学项目里 SHA-256 可以接受但你要知道完善的方案长什么样心里有个谱。3.3 追加写入与重复检查的顺序问题这里我想专门讲一个容易踩的坑重复用户名检查一定要在写入之前做而且检查的数据必须来自文件本身。很多初学者会写成这样先让用户输入用户名和密码然后立即打开文件追加完全不做重复检查。结果是同一个用户名可以注册十次登录的时候dict只会保留最后一个前面的注册记录看起来还在实际已经失效文件和用户认知完全对不上。正确的注册函数如下def register(username, password, filepathUSER_FILE): ok, msg validate_username(username) if not ok: return False, msg if len(password) 6: return False, 密码长度至少6位 users read_users(filepath) if username in users: return False, 用户名已存在 salt secrets.token_hex(16) hashed hash_password(password, salt) append_user(username, salt, hashed, filepath) return True, 注册成功关键点是read_users在写入前先执行把文件里已有的用户全部读进内存然后判断username in users。这里有一个先读后写的时序问题理论上在检查完和写入之间如果有另一个进程也注册了同名用户还是会产生重复。这就是并发问题文本文件方案天然很难完美解决我会在第 5 章详细讲。另外要留意一个细节读取整个文件再判断对于这个小项目完全够用。如果文件变成几十万行这个方案就不合适了那会儿你应该考虑数据库的唯一索引而不是继续在应用层做判断。3.4 注册分支的完整代码最后把注册分支放进一个简单的交互式菜单里def main(): while True: print(\n1. 注册 2. 登录 3. 退出) choice input(请选择).strip() if choice 1: username input(请输入用户名).strip() password input(请输入密码).strip() ok, msg register(username, password) print(msg) elif choice 2: username input(请输入用户名).strip() password input(请输入密码).strip() ok, msg login(username, password) print(msg) elif choice 3: break else: print(无效选择请重新输入) if __name__ __main__: main()这里我把密码输入单独拎出来是为了后续能替换成getpass之类的隐藏输入方式。实际使用的时候可以让密码不显示在终端上避免旁边有人偷看这是真实系统里非常基础的安全习惯。4. 登录功能落地比对只是表面关键是别泄露信息注册做完登录反而更简单但简单不等于可以随便写。登录的核心是根据用户名找到记录再比对密码哈希这个逻辑不复杂真正复杂的是异常情况和安全性处理。4.1 登录验证的代码逻辑登录函数如下def login(username, password, filepathUSER_FILE): users read_users(filepath) record users.get(username) if record is None: return False, 用户名或密码错误 salt, hashed record if hash_password(password, salt) hashed: return True, 登录成功 return False, 用户名或密码错误整个流程可以拆成三步读文件得到用户字典 → 用users.get(username)查找记录 → 重新计算密码哈希并比对。注意我用的是get而不是users[username]这样用户不存在时返回None而不是抛KeyError。这里有一个很多人忽略但非常重要的点比对的时机。密码比对必须在找到用户之后立刻进行而且比对用的是取出盐值、拼接输入密码、再哈希绝对不能直接把存的哈希拿出来和用户输入的密码比——存的本来就是哈希你拿哈希和明文比永远比不成功。我见过不少新手卡在这里纠结半天发现是比对对象选错了。4.2 为什么错误提示必须统一为用户名或密码错误这个细节我说给每一个写登录功能的人当用户不存在时提示用户名或密码错误当密码错误时也提示用户名或密码错误。两个场景共用一个提示不是偷懒是安全设计。原因很简单如果用户不存在时提示用户名不存在密码错误时提示密码错误那任何人都可以通过反复尝试收集到大量有效用户名。拿到用户名列表之后下一步就是针对性地爆破密码。统一提示等于把这条信息泄露的路堵死。这个案例在安全圈里已经讲过无数遍但落实到一行return字符串时还是有很多人写错。这也是我在第 3 章注册阶段就要求用户名唯一的原因之一。用户名是公开信息密码是机密信息两者在错误提示上必须严格区分对待。4.3 登录成功之后的会话怎么处理登录验证通过之后代码只返回了一个登录成功但在真实系统里这里要做的事远不止输出一句话。你需要考虑登录成功后怎么记住这个用户下次操作怎么知道当前是谁最简单的方案是用一个全局变量记录当前登录用户current_user None def login(username, password, filepathUSER_FILE): global current_user ok, msg login_logic(username, password, filepath) if ok: current_user username return ok, msg登录之后的操作都可以先检查current_user是否为None不是的话再放行。这个会话状态在命令行项目里用变量就能解决在 Web 项目里则会换成 Session 或 Token 机制。虽然教学项目不需要做那么重但我建议在代码里留出这个抽象哪怕只是加一个current_user变量后续想扩展登录态检查也会方便很多。登录成功后还有个容易被忽略的小操作密码输入错误次数多了要不要限制比如连续输错 5 次就锁定账号 10 分钟。这个功能对文本文件存储来说实现成本略高但不妨碍你先在用户字典里加一个失败次数字段思路有了代码都是水到渠成的事。我在第 6 章会具体展开。5. 实操中一定会踩的坑文件编码、空行、并发写这个项目最难的部分其实不是注册和登录逻辑而是文件本身。文件读写看起来基础一踩坑就直接动摇整个功能。我把实操中遇到的和帮别人排查过的典型问题集中放在这里。5.1 编码问题为什么打开文件全是乱码第一次写完代码在本机跑得好好的换个环境一运行中文字符全部变成乱码。问题基本都出在读的时候没指定编码写的时候也没指定编码。Python 在 Windows 上默认编码是gbk在 Linux 和 macOS 上是utf-8。如果你的程序在 Windows 上创建文件之后拿到 Linux 上用默认编码读取编码不一致必乱无疑。解决方案就是我在最开始强调的所有文件操作统一显式声明encodingutf-8。with open(filepath, a, encodingutf-8) as f:这个习惯看着不起眼但在跨平台项目里能帮你省掉非常多麻烦。顺带一提打开文件以后先把内容print出来确认一下排错效率远高于瞎猜。5.2 空行和末尾换行符的处理文件里的空行是初学者的第一个隐形杀手。我见过有人手滑在 user_g.txt 末尾多敲了一个空行结果注册的时候一切正常登录的时候死活找不到用户。排查下来发现是read_users没有跳过空行把一个空字符串当成了用户名记录。解决办法就是我前面代码里写的line line.strip() if not line: continuestrip()同时解决了另一个问题去掉行尾\n。如果不去掉哈希串末尾多了一个换行长度变成 65和注册时存的 64 位哈希完全对不上比对必然失败。这一类问题排查起来特别隐蔽因为你看文件内容的时候行尾的不可见字符很难用肉眼发现。所以我会建议所有的读文件代码都养成先 strip 再判断空行的习惯。不管文件是不是你程序生成的都要按外部不可信数据来处理。5.3 并发写用户文件时可能出现的覆盖这个坑在单机作业里一般遇不到但一旦部署成多人使用的服务马上就会暴露。问题的根源在于append_user和read_users是两个独立操作中间没有任何锁机制。想象一下这个场景用户 A 和用户 B 同时注册进程 A 先读了文件、检查用户名、准备追加进程 B 也读了文件、检查用户名、准备追加。两个进程同时以追加模式写同一个文件操作系统层面的追加是原子的但应用层的检查后写入并不是。更危险的情况是如果你用了读文件到内存 → 修改内存 → 整体写回文件的方案两个进程同时写回后写的一方会直接覆盖先写的一方其中一个用户的注册记录彻底丢失。这就是经典的读-改-写竞态问题。追求极致安全的做法是给文件加锁。在 Linux 环境下可以用fcntl对一个锁文件做独占锁Windows 下也可以用msvcrt.locking但跨平台又是个麻烦事。对教学项目来说有个折中思路不要把读取整个内存再整体写回作为默认写入策略用追加模式写入只动文件尾部冲突范围会小很多同时可以在写入前再检查一次用户名虽然不能根治并发问题但能降低出问题的概率。5.4 用临时文件 替换实现安全写入如果你坚持要支持整体写回的场景比如要修改某个用户的信息那我建议用临时文件 原子替换的方式而不是直接往原文件写。def write_users_safely(users, filepathUSER_FILE): tmp_path filepath .tmp with open(tmp_path, w, encodingutf-8) as f: for username, (salt, hashed) in users.items(): f.write(f{username},{salt},{hashed}\n) os.replace(tmp_path, filepath)核心思路是先写一个全新的临时文件写完后用os.replace一次性替换旧文件。os.replace在同一个文件系统内是原子操作要么替换成功要么替换失败不会出现写到一半程序崩溃旧文件和新文件都坏了的中间状态。这个模式在日常开发里非常常用。数据库事务解决的是类似的问题文件系统层面能提供的最低限度事务就是这一招。我在做这个功能的时候一开始也是直接往原文件写后来模拟了一次写入中途断电立刻发现文件被截成了半截从那以后就再也不敢裸写原文件了。6. 验收之后还能怎么改从教学 Demo 到可落地的小系统如果你的目标是交作业做到第 5 章其实已经超额达标了。但如果你是真心想把文件存储这套方案用到稍微真实一点的场景里下面这几个改进方向值得继续做而且难度都不高。6.1 把哈希算法升级成专门的口令哈希函数第 3 章我提到过 SHA-256 的局限它太快了不适合做密码哈希。如果你想把安全等级往上提可以试试标准库里的hashlib.scrypt或者安装bcrypt库。scrypt的好处是它是标准库自带的专门为口令设计自带盐值参数和 CPU 开销控制。def hash_password_scrypt(password, salt): return hashlib.scrypt( password.encode(utf-8), saltsalt.encode(utf-8), n2**14, r8, p1 ).hex()调高n参数计算变慢破解成本变高。真正的生产系统就是这么干的用计算时间来换取攻击者的成本。对这个教学项目来说你只要能讲清楚为什么要用慢哈希替代快哈希就已经比市面上很多半吊子教程专业了。6.2 加上登录失败锁定和账号状态字段第 4 章末尾提到的失败次数限制落到文件里其实就是加一个字段。比如把存储格式扩成用户名,盐值,密码哈希,失败次数,锁定状态添加字段后注意老数据怎么办读文件时parts长度可能只有 3也可能是 5所以解析逻辑要保持兼容。比较好的做法是给缺失字段补默认值if len(parts) 3: username, salt, hashed parts fail_count, locked 0, 0 elif len(parts) 5: username, salt, hashed, fail_count, locked parts这就是我为什么在第 2 章强调行尾追加字段的兼容性价值。只要你的解析代码对字段数量做了容错老用户数据不用迁移也能继续用。这种向后兼容的意识在真实项目里比多写几个功能都金贵。6.3 从文件存储平滑迁移到数据库终于做到需要换数据库的一天你会发现前面抽象的read_users、append_user这些接口成了救命稻草。只要注册和登录的业务代码不直接操作文件而是调用这几个函数迁移时只需要把函数内部实现替换成 SQL 语句业务层一行不用改。def register(username, password, filepathUSER_FILE): # 业务逻辑不变 ... # 只有 storage 层从 append_user 换成 insert into users(...)这也是我在全文反复强调封装和抽象的原因。初学者最容易犯的错就是把文件读写逻辑直接铺在注册函数里等数据量大了想换数据库等于重新写一遍。而一开始花十分钟做一层薄薄的封装后面能省下几个小时的重构时间。代码的设计价值往往不在写出来的那一刻而是在改需求的那一刻。最后再说一个我自己的心得这个项目虽然小但它把数据持久化、输入校验、安全哈希、异常处理这些基础能力全部串了起来。做的时候别急着抄代码先自己画一张流程草图把数据从哪来、存到哪去、怎么验证这条线理清楚再动手写。等你把 user_g.txt 改成 SQLite、再加上一个 Web 界面回头再看最初的这一百来行代码你会发现自己对一个用户系统是怎么跑起来的已经有了完整的认识。这比单纯会写一个登录接口值钱得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询