4. 登录功能实现:用户名密码校验、记住我功能、登录失败次数限制
登录功能,说白了就是系统的大门。门设计得不好,要么用户进不来,要么坏人随便进。我这些年做过的项目里,登录这块踩过的坑最多,也最有意思。
今天咱们就聊聊三个核心点:用户名密码怎么校验、记住我功能怎么实现、以及登录失败次数怎么限制。嗯,一个一个来。
4.1 用户名密码校验——别只比字符串
很多人觉得校验就是「用户输入的用户名和密码,跟数据库里存的一比对,完事」。其实没那么简单。
我习惯把校验拆成三步:
- 格式校验——用户名不能为空、密码长度够不够、有没有特殊字符
- 存在性校验——这个用户到底注册过没有
- 密码匹配校验——用哈希算法比对,而不是明文比对
你想想看,如果第一步格式校验都没过,就直接去查数据库,那不是浪费资源吗?
核心原则:密码永远不要存明文。用 bcrypt、Argon2 这类慢哈希算法。我见过有人用 MD5 存密码,说实话,那跟没锁门差不多。
下面是一个简单的校验逻辑示例:
def validate_login(username, password):
# 第一步:格式校验
if not username or len(username) < 3:
return {"success": False, "msg": "用户名至少3个字符"}
if not password or len(password) < 6:
return {"success": False, "msg": "密码至少6个字符"}
# 第二步:查询用户
user = db.query("SELECT * FROM users WHERE username = ?", username)
if not user:
return {"success": False, "msg": "用户名或密码错误"}
# 第三步:密码比对(用 bcrypt)
if not bcrypt.checkpw(password.encode(), user.password_hash.encode()):
return {"success": False, "msg": "用户名或密码错误"}
return {"success": True, "user": user}
注意看,用户名不存在和密码错误,我返回的是同一个提示。为什么?防止坏人通过提示信息猜出哪些用户存在。这是安全设计的基本常识。
4.2 记住我功能——别用简单的布尔值
「记住我」这个功能,很多新手直接用一个 checkbox 的布尔值来控制。其实这里有个坑。
我曾经在一个项目中,直接用 localStorage.setItem('rememberMe', true) 来存。结果呢?用户勾选了记住我,关掉浏览器再打开,登录状态还在。但问题是,这个 token 永远不会过期。用户如果在公共电脑上登录……嗯,后果你懂的。
正确的做法是:
- 生成一个长期有效的 token(比如30天),存到数据库
- 把这个 token 写入客户端的 cookie(设置 httpOnly、Secure 标志)
- 每次请求时,检查这个 token 是否有效、是否过期
我的习惯:记住我的 token 和普通 session token 分开存储。普通 token 有效期2小时,记住我 token 有效期30天。这样即使记住我 token 泄露,损失也有限。
代码实现大概长这样:
def handle_remember_me(user_id, remember_me):
if remember_me:
# 生成一个30天有效的 token
token = secrets.token_urlsafe(32)
expires_at = datetime.now() + timedelta(days=30)
db.execute(
"INSERT INTO remember_tokens (user_id, token, expires_at) VALUES (?, ?, ?)",
user_id, token, expires_at
)
# 设置 cookie,有效期30天
response.set_cookie('remember_token', token,
expires=expires_at,
httponly=True,
secure=True)
else:
# 普通登录,只生成 session
session['user_id'] = user_id
4.3 登录失败次数限制——别让暴力破解得逞
这个功能,说白了就是防暴力破解的。我见过最粗暴的做法是「连续输错3次就永久封号」——结果用户自己把自己封了,还得找客服解封,体验极差。
我个人建议用渐进式锁定策略:
| 连续失败次数 | 锁定时间 | 说明 |
|---|---|---|
| 1-3次 | 无锁定 | 正常输入错误,给提示就行 |
| 4-5次 | 30秒 | 轻微限制,防止快速重试 |
| 6-8次 | 5分钟 | 明显限制,用户可能需要冷静一下 |
| 9次以上 | 30分钟 | 严重限制,建议同时触发验证码 |
为什么会这样设计?因为真正的暴力破解工具,每秒能试几百次。你锁定30秒,对它来说跟没锁一样。但锁定5分钟,它就受不了了。
注意:锁定策略要基于「用户名+IP」双重维度。只锁用户名的话,坏人可以换着IP试。只锁IP的话,一个公司的人共用同一个IP,一个人输错了全公司都登不了。我当年就吃过这个亏。
实现逻辑可以参考:
def check_login_attempt(username, ip_address):
key = f"login_fail:{username}:{ip_address}"
attempts = redis.get(key) or 0
if attempts >= 9:
return {"locked": True, "wait_minutes": 30}
elif attempts >= 6:
return {"locked": True, "wait_minutes": 5}
elif attempts >= 4:
return {"locked": True, "wait_seconds": 30}
return {"locked": False}
def record_failed_attempt(username, ip_address):
key = f"login_fail:{username}:{ip_address}"
redis.incr(key)
# 设置过期时间,防止无限累积
redis.expire(key, 1800) # 30分钟后自动重置
这里用 Redis 来存失败次数,而不是数据库。为什么?因为 Redis 快,而且可以自动过期。用数据库的话,你还得写个定时任务去清理过期的记录,麻烦。
4.4 把三个功能串起来
好了,三个核心点都讲完了。咱们把它们串成一个完整的登录流程:
- 用户提交用户名、密码、是否记住我
- 检查登录失败次数——如果被锁了,直接返回「请稍后再试」
- 格式校验——用户名密码格式对不对
- 查询用户——用户存不存在
- 密码比对——哈希值匹配吗
- 如果失败——记录失败次数,返回错误提示
- 如果成功——清除失败次数,生成 session,处理记住我 token
最后说一句:登录功能看着简单,但它是系统的第一道防线。我见过太多项目因为登录做得太糙,上线第一天就被撞库了。花点心思在这上面,绝对值得。
下一章咱们聊聊权限管理的核心——RBAC 模型怎么落地。到时候我会分享一个我踩过的坑,关于角色继承的,挺有意思。