☰
不引数据库,用 user_g.txt 实现注册登录与密码安全存储
2026/10/13 13:57:31 网站建设 项目流程

最近有朋友问我,他想做一个最简单的注册登录功能,不引数据库,不上框架,只要用户输入用户名和密码,程序能把它们存下来,下次还能用同一套凭证登录。他特别强调一个细节:用户名和密码要存在一个叫 user_g.txt 的文本文件里。

这个需求太典型了,几乎每个写代码的人都绕不过这一关。无论是课程设计、毕业设计,还是自己写个内部小工具、给某个业务系统做个最小可用版本,注册登录都是第一道要跨过的门槛。很多人第一次接触它,往往不是技术难度大,而是不知道从哪下手:数据怎么存?存文件还是存内存?密码要不要加密?登录的时候怎么验证?这些问题如果没有想清楚,代码写出来可能会跑通,但一换场景就漏洞百出。

这篇内容我把整个思路拆开讲,从 user_g.txt 的存储格式,到注册、登录流程的完整实现,再到实际开发中一定会遇到的坑,都按我自己的实操习惯给了一遍。适合刚接触编程、正在做课设,或者需要快速给项目加一套认证模块的同学参考。代码用 Python 写,简单直接,你用其他语言也能按同样思路搬。

1. 项目整体拆解:注册登录到底在做什么

很多人一上手就写代码,结果写出来只是“看起来能跑”,却没搞明白注册登录的本质。我建议大家先停下来,把需求拆成几个问题。

1.1 注册和登录要解决的三个基本问题

注册登录本质上只解决三件事:身份标识、凭证校验、状态区分。

身份标识就是“你是谁”。通常我们用用户名来代表一个用户。程序必须保证用户名是唯一的,否则登录时无法判断到底该匹配谁。所以注册流程里的“查重”不是可选操作,而是核心逻辑。

凭证校验就是“证明你是你”。最常见的凭证是密码。系统不能把用户输入的密码拿来直接当凭证,也不能仅仅在内存里比对一下就结束,而是应该把密码以安全的形式持久化到某个地方。下次用户登录时,再拿他输入的密码和持久化数据做比较,一致就通过。

状态区分就是登录成功之后,程序知道自己已经处于“已登录”状态。这个在命令行程序里通常简单处理——返回一个布尔值,或者把当前用户写到全局变量里。如果是 Web 系统,则会引入会话(Session)或令牌(Token),但原理一样。

我们的 user_g.txt 版本,就是围绕“注册时写入文件、登录时读取文件”来完成的,属于最朴素的实现方式。

1.2 为什么选择 user_g.txt 而不是数据库

很多人会问:为什么不用 SQLite?不用 MySQL?不用 Redis?问题的答案是:这个需求的应用场景决定了存储选型。

文本文件存储最大的优势是零依赖、零配置。你不需要装数据库服务,不需要建表写 SQL,不需要引入 ORM 框架。只要操作系统能创建文件,程序就能跑起来。尤其在做课程设计或者本地开发验证时,这种“打开文件就能看到数据”的直观性非常舒服。

但文本文件的劣势也很明显:查询效率低、并发写入不安全、缺少事务管理、数据结构脆弱。一两个用户量时毫无感知,如果同时有几十个请求写入 user_g.txt,很可能出现互相覆盖、数据丢失的问题。所以它只适合练习、原型验证和小型单机工具,一旦需要多人同时用,请尽早换数据库。

有人还问过我用 JSON 文件行不行。其实也行,原理一样,只是解析方式不同。文本文件、JSON、CSV 本质都是“自行管理持久化”,差别只在格式约定和读写解析的便利性。

1.3 这个功能的真实适用场景

我见过很多下游场景都会先用这种简单方式起步。比如某个自动化运维脚本,需要一个管理员账号来限制功能使用;比如某高校同学做的二手交易小系统,先本地跑通注册登录,再整体迁移到数据库;比如企业内网的小工具,不想引入额外的数据库服务,就先用文件存储顶一阵子。

我要强调一点:文本文件存储注册信息,可以用于学习、演示、内部测试,但不要直接部署到面向大量真实用户的生产环境。生产环境对数据一致性、安全性和并发能力要求非常高,文件系统承担不了这种责任。明白这个边界,用起来才不会踩大坑。

2. 存储格式设计:user_g.txt 怎么组织内容

文件存储并不是把数据随便往文件里一行一写就完事。你要先设计好格式,否则读取时根本没法正确解析出哪些是用户名、哪些是密码。

2.1 字段分隔方案:用什么符号分隔用户名和密码

user_g.txt 本质就是一个简单的“表格”,每一行对应一个用户。所以每一行至少要包含两个字段:用户名、密码(或密码摘要)。字段之间必须有一个约定好的分隔符。

常见做法有几种:逗号分隔、制表符分隔、竖线分隔。我推荐用竖线|,因为用户名和密码中不太可能包含竖线,比逗号更安全。如果用户名里允许输入逗号,解析时就会出问题,竖线能降低这种风险。

一行数据大概是这个样子:

alice|8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918

这是 alice 用户存储后的记录,竖线左边是用户名,右边是密码摘要,而不是密码原文。这样设计之后,读取程序很容易按行拆解:

  • line.split("|")把一行拆成两个部分
  • 第一个字段作为用户名
  • 第二个字段作为密码校验值

同时还要处理一个细节:为什么是竖线而不是空格?因为用户名和密码都可能包含空格,用空格分隔会让解析不稳定。我通常还会在代码里做一次strip(),去掉用户输入时不小心带上的首尾空格,避免“明明输入对了却登录失败”的情况。

2.2 密码直接明文写的风险与哈希保存方案

这里有一个很容易被忽视的问题:标题写的是“用户名、密码存储在 user_g.txt 中”,但密码到底存原文还是存摘要?

如果按字面意义把密码原文写进文件,比如:

alice|123456

那一旦这个文件被其他人看到,所有账号密码就全部裸奔了。更糟糕的是,很多人在不同平台会复用同一个密码,攻击者拿到一个文件,可能连你其他系统都能登录进去。这就是为什么任何正经系统都不会明文存储密码。

我建议的方式是:存储密码的哈希值。哈希是一种单向转换,同一个密码固定对应同一个哈希值,但反过来几乎无法从哈希值还原出密码。注册时把用户输入的密码转成哈希,再写进 user_g.txt;登录时把用户输入的密码也转成哈希,再和文件里的哈希比对。

哈希方案的实现非常简单,Python 里用hashlib就能搞定。这样,即使文件泄露,攻击者拿到的也只是哈希值,靠暴力破解的成本会高很多。虽然无盐哈希对于简单密码仍有风险,但至少比明文存储安全了几个量级。更稳妥的加盐方案,我在后面专门讲。

2.3 文件路径、编码和初始化

user_g.txt 的文件位置决定了程序在哪个目录下运行才能找到它。最简单的方式是使用相对路径,也就是直接把文件名写在代码里,默认程序的工作目录就是当前项目目录。这样适合小项目,但如果你在别的目录启动程序,文件就可能找不到。为了避免这个问题,我通常会在代码里判断文件是否存在,不存在就先创建。

还有一个容易踩的坑是编码。文本文件必须明确指定编码,否则在 Windows 下默认可能是 GBK,在 macOS 或 Linux 下默认是 UTF-8,跨平台一跑就乱码。我的习惯是统一用utf-8打开和写入文件,代码里明确写encoding="utf-8"。

初始化方面,注册功能第一次运行时,user_g.txt 可能还不存在。写入的时候用"a"模式打开,文件不存在时会自动创建;读取的时候就不能直接打开不存在的文件,要先判断。所以需要写一个“如果文件不存在就返回空数据”的逻辑。

3. 注册登录全流程实现(代码走读)

接下来进入正题。我按“辅助函数 -> 注册 -> 登录 -> 主循环”的顺序来写,每一段都可以直接复制到你的项目里跑。

3.1 准备工作:项目目录和两个辅助函数

建议你新建一个目录,比如simple_auth,在目录下创建一个auth.py文件,下一步所有代码都写在这个文件里。

首先要引入三个标准库:os用来判断文件是否存在,hashlib用来做哈希,getpass用来隐藏输入密码。

辅助函数有两个,一个是读取 user_g.txt 并转换成内存里的字典,一个是把新用户追加写入文件。为什么用字典?因为登录时要根据用户名快速查找,字典是最直观的结构,键是用户名,值是密码哈希。

import os import hashlib import getpass FILE_NAME = "user_g.txt" def load_users(): users = {} if not os.path.exists(FILE_NAME): return users with open(FILE_NAME, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue parts = line.split("|") if len(parts) == 2: username = parts[0].strip() password_hash = parts[1].strip() users[username] = password_hash return users def save_user(username, password_hash): with open(FILE_NAME, "a", encoding="utf-8") as f: f.write(f"{username}|{password_hash}\n")

这里有个细节值得注意:load_users会把每一行都拆开,如果某一行格式不对就直接跳过。这个“容错处理”很重要,因为 user_g.txt 可能被手工编辑过,格式乱了不能让整个程序崩溃。save_user使用"a"追加模式,每次注册一个用户就在文件末尾加一行,不会覆盖原有数据。

3.2 注册流程实现

注册函数的逻辑有四步:输入用户名、检查用户名是否存在、输入密码并校验长度、计算哈希并保存。

def register(): users = load_users() username = input("请输入用户名:").strip() if not username: print("用户名不能为空") return if username in users: print("用户名已存在,请换一个") return password = getpass.getpass("请输入密码:").strip() if len(password) < 6: print("密码长度不能少于6位") return password_hash = hashlib.sha256(password.encode("utf-8")).hexdigest() save_user(username, password_hash) print("注册成功")

注册时为什么要先load_users()?因为只有先知道现有用户,才能做查重。如果不查重,同一个用户名可能出现多条记录,登录时程序不知道该返回哪一条,逻辑就会混乱。

密码长度校验我也建议保留。通常最少 6 位,这在真实项目里还远远不够,但对于一个命令行练习程序,已经能挡掉不少随手输入的错误。

哈希计算这一行是关键:先要把密码字符串编码成字节,再交给sha256,然后取hexdigest()得到一串十六进制字符。这个值就是要写进 user_g.txt 的内容。

3.3 登录流程实现

登录函数做的事情正好是注册的逆过程:读取现有用户,按用户名找到记录,把用户输入的密码哈希后与文件中的值比对。

def login(): users = load_users() username = input("请输入用户名:").strip() if username not in users: print("用户名不存在") return False password = getpass.getpass("请输入密码:").strip() password_hash = hashlib.sha256(password.encode("utf-8")).hexdigest() if password_hash == users[username]: print("登录成功") return True print("密码错误") return False

这里有个用户体验上的细节:当用户名不存在时,我直接提示“用户名不存在”。但如果在安全要求更高的系统里,通常不会告诉用户到底是用户名错了还是密码错了,而是统一提示“用户名或密码错误”,避免攻击者通过提示信息探测有效用户名。命令行教学项目可以保留这种提示,方便调试,但如果要拿去做正式系统,建议改成模糊提示。

登录比对的核心就是哈希值相等判断。字符串比较本身很简单,真正比较容易出错的地方是文件里的数据可能带空格、空行,或者编码不一致。所以前面load_users里做了strip(),这里用户输入也做了strip(),两边都清理干净,登录才能稳定。

3.4 主控流程整合

有了注册和登录两个核心函数,还需要一个入口,让用户能选择操作。我用一个while循环做菜单,数字 1 注册、2 登录、3 退出。

def main(): while True: print("\n===== 用户系统 =====") print("1. 注册") print("2. 登录") print("3. 退出") choice = input("请选择:").strip() if choice == "1": register() elif choice == "2": login() elif choice == "3": print("再见") break else: print("无效选择,请重新输入") if __name__ == "__main__": main()

为什么一定要if __name__ == "__main__"?因为这样你的代码既可以作为脚本直接运行,也可以在别的程序里当作模块导入,不会在导入时马上执行菜单。这个习惯从写第一个工具类脚本时就应该养成。

到这一步,注册登录功能已经完整跑通了。运行auth.py,注册一个用户,关闭程序再重新打开,输入同样的用户名和密码,能够登录成功,说明数据确实持久化到了 user_g.txt 中。

4. 常见问题排查与避坑实录

这段是实操中一定会遇到的高频问题,我按“现象 -> 原因 -> 解决”的方式整理成了表格,方便你对照排查。

现象原因解决方案
运行时提示文件找不到没有处理文件不存在的情况在读取前用os.path.exists判断,或先写入创建
密码没有隐藏,直接显示在终端上某些 IDE 或调试环境下getpass不生效临时改用input,或确保在系统原生终端运行
中文用户名写入后变成乱码文件编码没有指定为 UTF-8读写都加上encoding="utf-8"
第二次注册同名用户成功了注册前没有加载已有用户做查重在register()里先调用load_users()
登录时总是提示密码错误用户名或密码带了空格统一对输入和文件内容做strip()
明明注册成功了,重开程序数据没了当前工作目录与文件所在目录不一致使用绝对路径,或检查运行命令所在目录
文件被手工编辑后程序崩溃某一行分隔符缺失或格式不对解析时检查 `split("

4.1 getpass 在 IDE 里失效怎么办

这是最常遇到的问题。很多同学在编辑器自带的终端里运行程序,发现getpass.getpass()不生效,密码还是明文显示,甚至直接报错。因为某些 IDE 的控制台不完全支持隐藏输入机制。

解决办法很直接:如果只是调试逻辑,可以临时把getpass.getpass换成input,等调试完再改回来。或者干脆不用getpass,使用input("请输入密码:"),但这样密码会显示在屏幕上。我的习惯是写一个统一函数,在兼容环境下用getpass,遇到异常就降级成input,这样就不会被环境卡住。

4.2 文件被手工编辑后程序崩溃

user_g.txt 是普通文本文件,测试的时候很容易手滑改错。比如某一行只有用户名,没有密码,或者多了一个分隔符,split("|")之后字段数量对不上,程序就可能报索引越界。

代码里已经做了防御:解析时先判断len(parts) == 2,不对就跳过。如果你用的是自己的实现,一定要记得加这个判断。它不麻烦,但能让程序健壮很多。

4.3 用户名大小写和空格导致登录失败

文件里存的是Alice,用户登录时输入alice,按当前逻辑会被当成两个不同的用户。要不要区分大小写,取决于产品设计。如果你的系统希望用户名不区分大小写,可以在注册和登录时统一转成小写再处理,例如username.lower()。但要注意,注册时转换后,文件里存的就是小写用户名,显示时可能和用户原本输入的样式不一样。这个取舍在真实系统里需要仔细设计。

空格问题更隐蔽。用户名开头或结尾带一个空格,肉眼根本看不出来,但比对时却会失败。所以我在所有用户输入的地方都加了strip(),注册时清一次,登录时清一次,比对时也清一次。每一次清理都是在减少人为数据错误的可能性。

4.4 多用户并发写入的隐患

如果你的程序只是为了本地练习,并不会有真正的并发请求。但你可能会把同一个脚本复制成两个进程同时跑,或者后续想加一个 Web 接口,这时候写文件就容易出问题了。两个进程同时打开文件追加,可能互相覆盖,轻则丢数据,重则文件内容错乱。

简单方案是引入文件锁,比如filelock库;但在命令行版教学代码里,我会建议:如果已经有并发需求,就不要再继续用文件存储了,尽快迁移到 SQLite 或真正的数据库。文本文件不是为并发设计的,硬撑只会给自己增加不必要的问题。

5. 扩展思路:从文本文件走向真实工程

如果你已经跑通了上面的代码,下一步可以考虑这些方向。它们不是必须做,但能帮你理解真实系统的演化路径。

5.1 加盐哈希升级

无盐sha256对简单密码并不安全,因为攻击者可以用彩虹表批量反查。更安全的做法是引入“盐”:每个用户生成一个随机字符串,把盐和密码拼在一起再做哈希,然后把用户名、盐、哈希三个字段都存进 user_g.txt。

存储格式可以变成:

alice|a1b2c3d4e5f6g7h8i9j0|8c6976e5b5...

注册时生成随机盐;登录时先从文件读出盐,再和用户输入的密码拼接、哈希、比对。这样即使两个用户密码相同,哈希值也会不同;即使攻击者拿到了文件,用现成彩虹表也很难破解,因为哈希里混入了每个用户独立的随机盐。

需要提醒的是,盐一定要随机,不要用固定字符串,否则加盐就失去了意义。Python 标准库里的secrets.token_hex()可以生成安全的随机字符串。

5.2 从文件到 SQLite 或数据库

文件存储解决了“能跑”的问题,但没有解决“好维护”的问题。当你需要查询大量用户、按条件筛选、支持并发写入时,换数据库是必然选择。

SQLite 是很好的中间方案,不需要单独安装服务,只是一个本地文件,但支持 SQL 建表、事务、并发控制。迁移思路也很简单:建一张users表,字段是id, username, password_hash, salt, created_at,把原本读写 user_g.txt 的逻辑换成数据库插入和查询语句。

换成数据库后,注册流程基本不变,仍然是查重、哈希、插入。登录流程也基本不变,仍然是按用户名查询、比对哈希。你会发现,真正复杂的东西其实是业务逻辑,而不是存储介质。这也是为什么要先在小项目里把注册登录想清楚。

5.3 登录之后的“会话”怎么办

当前这个命令行版本,登录成功只是返回了一个True,并没有记住“当前用户”。如果你要在项目里继续做“登录后才能访问的功能”,就需要设计会话机制。

最简单的做法是在main里定义一个current_user变量,登录成功时写入用户名,然后根据它判断是否允许执行受保护的操作。更工程化的做法是使用会话 ID 或令牌,比如登录成功后生成一个随机 token,后续请求带上这个 token,服务端再判断 token 是否有效、属于哪个用户。

不管怎么做,核心概念是一样的:登录只是认证的一瞬间,会话才是持续状态;认证解决“你是谁”,授权解决“你能做什么”。

6. 一点个人体会

user_g.txt 这个需求看起来简单,但每次写都有新的收获。我最早做这类功能时,也直接把密码明文写进了文件,后来被身边朋友提醒才意识到问题有多严重。从那以后,我接手任何和用户认证相关的功能,第一反应不是怎么写登录接口,而是先想清楚凭证该怎么安全存放。

现在再回头看,文件存储、哈希校验、并发控制、会话管理,这些知识点完全可以由一个简单的 user_g.txt 串起来。你在命令行里把它跑通一遍,再去理解数据库存储、鉴权框架、Token 机制,会轻松很多。所以别嫌这个题目小,真正把它吃透,后面的路会顺得多。

如果你在自己的项目里碰到了其他奇怪的问题,不妨先检查最基本的文件内容长什么样、有没有空格、编码对不对。大多数 Register/Login 的事故,最后都出在这些不起眼的细节上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询