第4章 4.5 综合实战:登录注册系统
🎯 开场:为什么你每次打开网站都要"登录"?
上一章我们学会了怎么用 Python 发送邮件、生成验证码。但是发完验证码就结束了吗? 当然没有——用户输入验证码后,你的网站还得"记住"他是谁,接下来几个小时甚至几天,他再访问网站都不用重新输入密码。
这个"记住用户"的能力,就是今天要解决的核心问题。
你有没有遇到过这种情况:
- 在网吧登录了邮箱,走的时候没点"退出",结果下个人直接登上你的邮箱了
- 网站提示"记住我",勾选了之后一个月不用登录,没勾选关掉浏览器就得重来
这背后的原理,就是 Session 和 Cookie。 今天我们要用 Python 做一个完整的登录注册系统,包含:
- 验证码校验(接上章)
- Session 管理(服务器端记住用户)
- Cookie 机制(浏览器端保存登录状态)
- 完整的注册→登录→登出流程
学完这章,你就能写出一个真正能用的用户系统了。
🧱 基础:三个核心概念,像钥匙、门禁卡和登记簿
1. Sessio\n\n
\n\n
\n\nn(会话)= 酒店的临时登记簿
是什么:服务器端存储的用户信息,存着"这个浏览器是谁"。
为什么要用:HTTP 是无状态的——每次请求对服务器来说都一样,就像酒店前台不记得你是谁。Session 让服务器能"认出"同一个访客。
怎么用:
import secrets
# 创建一个 Session ID(就像给房间分配一个随机房号)
session_id = secrets.token_urlsafe(32)
print(f"生成的会话ID: {session_id}")
# 输出示例:生成的会话ID: Yr8fK2mP3xL...
secrets.token_urlsafe(32) 生成一个 32 字节的随机字符串作为会话 ID,这个 ID 就是用户的"临时身份证号"。
# 模拟一个简单的 Session 存储
sessions = {}
def create_session(username):
"""创建新会话"""
session_id = secrets.token_urlsafe(32)
sessions[session_id] = {
'username': username,
'login_time': '2024-01-15 10:30:00'
}
return session_id
def get_session(session_id):
"""根据ID获取会话信息"""
return sessions.get(session_id)
# 测试
sid = create_session('张三')
print(f"会话信息: {get_session(sid)}")
# 输出示例:会话信息: {'username': '张三', 'login_time': '2024-01-15 10:30:00'}
代码解释:
- sessions 是一个字典,键是 session_id,值是用户信息
- create_session 生成新会话并存入字典
- get_session 根据 ID 查会话,不存在返回 None
2. Cookie = 餐厅的会员卡
是什么:浏览器保存在用户本地的一段数据,下次访问网站时自动带上。
为什么要用:Session 存在服务器端,但服务器不记得"哪个浏览器"对应"哪个 session"。Cookie 就像会员卡——你拿着卡去餐厅,刷卡就知道你是几号会员。
怎么用:
from http.cookies import SimpleCookie
# 模拟服务器设置 Cookie
cookie = SimpleCookie()
cookie['session_id'] = 'Yr8fK2mP3xL...'
cookie['session_id']['httponly'] = True # 防止 JavaScript 读取(更安全)
cookie['session_id']['maxage'] = 3600 # 有效期 1 小时
cookie['session_id']['path'] = '/'
print("服务器设置的 Cookie 头:")
print(cookie.output())
# 输出示例:Set-Cookie: session_id="Yr8fK2mP3xL..."; HttpOnly; Path=/; Max-Age=3600
代码解释:
- SimpleCookie 是 Python 内置的 Cookie 处理类
- httponly=True 防止 XSS 攻击——脚本不能读取这个 Cookie
- maxage=3600 设置过期时间,不设置的话关掉浏览器就失效
# 模拟浏览器发送 Cookie(从本地存储读取并发送)
browser_cookie = 'session_id="Yr8fK2mP3xL..."'
print(f"浏览器发送的 Cookie: {browser_cookie}")
# 输出示例:浏览器发送的 Cookie: session_id="Yr8fK2mP3xL..."
浏览器每次请求都会自动带上 Cookie,服务器根据 Cookie 中的 session_id 就能找到对应的会话。
3. 登录 vs 注册 = 办卡和刷卡
注册:在数据库新建一个用户记录,相当于"办理会员卡"
登录:验证用户名密码,然后创建 Session,相当于"刷卡进门"
流程对比:
| 操作 | 做什么 | 数据往哪存 |
|---|---|---|
| 注册 | 创建新用户 | 用户数据库(CSV/JSON/MySQL) |
| 登录 | 验证密码 + 创建 Session | Session 存储 + 浏览器 Cookie |
| 登出 | 删除 Session | Session 存储 |
# 用 JSON 文件模拟用户数据库
import json
def save_user(username, password_hash):
"""保存用户到 JSON 文件"""
try:
with open('users.json', 'r') as f:
users = json.load(f)
except FileNotFoundError:
users = {}
users[username] = password_hash
with open('users.json', 'w') as f:
json.dump(users, f, indent=2)
print(f"用户 {username} 注册成功!")
def verify_login(username, password):
"""验证登录"""
import hashlib
try:
with open('users.json', 'r') as f:
users = json.load(f)
except FileNotFoundError:
return False
# 计算输入密码的哈希
password_hash = hashlib.sha256(password.encode()).hexdigest()
# 验证:用户名存在 且 密码哈希匹配
return username in users and users[username] == password_hash
# 测试注册和登录
save_user('小明', hashlib.sha256('123456'.encode()).hexdigest())
print(f"验证结果: {verify_login('小明', '123456')}")
# 输出示例:
# 用户 小明 注册成功!
# 验证结果: True
代码解释:
- 用 hashlib.sha256 存密码的哈希而不是明文,即使文件泄露也不知道原始密码
- 注册时存哈希,登录时把输入密码转成哈希再比较
🔥 实战:3 个递进项目
项目 1:5 分钟 - Session 和 Cookie 模拟器
跟着抄就能跑,理解核心 API。
import secrets
import hashlib
import json
from http.cookies import SimpleCookie
from datetime import datetime
class SimpleAuth:
"""极简登录注册系统"""
def __init__(self):
self.sessions = {} # session_id -> user_info
self._load_users()
def _load_users(self):
"""加载用户数据"""
try:
with open('users.json', 'r') as f:
self.users = json.load(f)
except FileNotFoundError:
self.users = {}
def _save_users(self):
"""保存用户数据"""
with open('users.json', 'w') as f:
json.dump(self.users, f, indent=2, ensure_ascii=False)
def register(self, username, password):
"""注册"""
if username in self.users:
return False, "用户名已存在"
password_hash = hashlib.sha256(password.encode()).hexdigest()
self.users[username] = password_hash
self._save_users()
return True, f"用户 {username} 注册成功"
def login(self, username, password):
"""登录"""
if username not in self.users:
return False, None, "用户名不存在"
password_hash = hashlib.sha256(password.encode()).hexdigest()
if self.users[username] != password_hash:
return False, None, "密码错误"
# 创建 Session
session_id = secrets.token_urlsafe(32)
self.sessions[session_id] = {
'username': username,
'login_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
return True, session_id, f"登录成功,欢迎 {username}"
def get_cookie_header(self, session_id):
"""生成 Cookie 头"""
cookie = SimpleCookie()
cookie['session_id'] = session_id
cookie['session_id']['httponly'] = True
cookie['session_id']['maxage'] = 3600
cookie['session_id']['path'] = '/'
return cookie.output()
def check_session(self, session_id):
"""检查 Session 是否有效"""
if session_id in self.sessions:
return self.sessions[session_id]
return None
def logout(self, session_id):
"""登出"""
if session_id in self.sessions:
username = self.sessions[session_id]['username']
del self.sessions[session_id]
return True, f"{username} 已退出登录"
return False, "Session 不存在"
# 测试
auth = SimpleAuth()
print("=== 测试注册 ===")
print(auth.register('张三', 'password123'))
print("\n=== 测试登录 ===")
success, session_id, msg = auth.login('张三', 'password123')
print(msg)
print(f"Session ID: {session_id}")
print(f"Cookie: {auth.get_cookie_header(session_id)}")
print("\n=== 测试验证 ===")
session = auth.check_session(session_id)
print(f"当前用户: {session['username']} (登录时间: {session['login_time']})")
print("\n=== 测试登出 ===")
print(auth.logout(session_id))
print(f"登出后验证: {auth.check_session(session_id)}")
预期输出:
=== 测试注册 ===
(True, '用户 张三 注册成功')
=== 测试登录 ===
登录成功,欢迎 张三
Session ID: xK9fL2mN4pQ...
Cookie: Set-Cookie: session_id="xK9fL2mN4pQ..."; HttpOnly; Path=/; Max-Age=3600
=== 测试验证 ===
当前用户: 张三 (登录时间: 2024-01-15 10:30:45)
=== 测试登出 ===
(True, '张三 已退出登录')
登出后验证: None
一句话解释:这个类把注册、登录、Session 管理都封装好了,Session 存在内存字典里,刷新就没了——所以生产环境要用数据库存。
项目 2:15 分钟 - 带验证码的完整登录注册
从 CSV 读数据,加验证码校验。
import csv
import secrets
import hashlib
import random
import string
from datetime import datetime
class CompleteAuth:
"""完整登录注册系统(带验证码)"""
def __init__(self):
self.sessions = {}
self.codes = {} # 存储验证码:username -> (code, expires_at)
self.users_file = 'users.csv'
self._init_users_file()
def _init_users_file(self):
"""初始化用户文件"""
try:
with open(self.users_file, 'r', encoding='utf-8'):
pass
except FileNotFoundError:
with open(self.users_file, 'w', newline='', encoding='utf-8'):
writer = csv.writer(self)
writer.writerow(['username', 'password_hash', 'created_at'])
def _load_users(self):
"""从 CSV 加载用户"""
users = {}
try:
with open(self.users_file, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
users[row['username']] = row['password_hash']
except FileNotFoundError:
pass
return users
def _save_user(self, username, password_hash):
"""保存用户到 CSV"""
with open(self.users_file, 'a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow([username, password_hash, datetime.now().isoformat()])
def generate_code(self, username, length=6):
"""生成验证码"""
code = ''.join(random.choices(string.digits, k=length))
# 模拟过期时间:5分钟后过期
self.codes[username] = {
'code': code,
'expires_at': datetime.now().timestamp() + 300
}
return code
def verify_code(self, username, code):
"""验证验证码"""
if username not in self.codes:
return False, "验证码未生成"
code_info = self.codes[username]
# 检查过期
if datetime.now().timestamp() > code_info['expires_at']:
del self.codes[username]
return False, "验证码已过期"
# 检查正确
if code_info['code'] != code:
return False, "验证码错误"
# 验证成功后删除验证码(只能用一次)
del self.codes[username]
return True, "验证成功"
def register_with_code(self, username, password, code):
"""带验证码的注册"""
# 先验证验证码
ok, msg = self.verify_code(username, code)
if not ok:
return False, msg
# 检查用户是否存在
users = self._load_users()
if username in users:
return False, "用户名已存在"
# 保存用户
password_hash = hashlib.sha256(password.encode()).hexdigest()
self._save_user(username, password_hash)
return True, f"用户 {username} 注册成功"
def login(self, username, password):
"""登录"""
users = self._load_users()
if username not in users:
return False, None, "用户名不存在"
password_hash = hashlib.sha256(password.encode()).hexdigest()
if users[username] != password_hash:
return False, None, "密码错误"
# 创建 Session
session_id = secrets.token_urlsafe(32)
self.sessions[session_id] = {
'username': username,
'login_time': datetime.now().isoformat()
}
return True, session_id, f"登录成功"
def get_logged_in_user(self, session_id):
"""根据 session_id 获取登录用户"""
session = self.sessions.get(session_id)
if session:
return session['username']
return None
def logout(self, session_id):
"""登出"""
if session_id in self.sessions:
username = self.sessions[session_id]['username']
del self.sessions[session_id]
return True, f"{username} 已退出"
return False, "无效的 Session"
# 测试完整流程
auth = CompleteAuth()
print("=== 1. 生成并验证验证码 ===")
code = auth.generate_code('李四')
print(f"李四的验证码: {code}")
print(f"验证结果: {auth.verify_code('李四', code)}")
print(f"再次验证(已删除): {auth.verify_code('李四', code)}")
print("\n=== 2. 注册流程 ===")
code = auth.generate_code('李四')
print(f"注册结果: {auth.register_with_code('李四', 'mysecret', code)}")
print("\n=== 3. 登录流程 ===")
success, session_id, msg = auth.login('李四', 'mysecret')
print(f"{msg}, Session: {session_id[:20]}...")
print("\n=== 4. 验证登录状态 ===")
print(f"当前用户: {auth.get_logged_in_user(session_id)}")
print(f"伪造Session: {auth.get_logged_in_user('fake_id')}")
print("\n=== 5. 登出 ===")
print(auth.logout(session_id))
print(f"登出后用户: {auth.get_logged_in_user(session_id)}")
预期输出:
=== 1. 生成并验证验证码 ===
李四的验证码: 482951
验证结果: (True, '验证成功')
再次验证(已删除): (False, '验证码未生成')
=== 2. 注册流程 ===
注册结果: (True, '用户 李四 注册成功')
=== 3. 登录流程 ===
登录成功, Session: xK9fL2mN4pQr8sT3...
=== 4. 验证登录状态 ===
当前用户: 李四
伪造Session: None
=== 5. 登出 ===
(True, '李四 已退出')
登出后用户: None
一句话解释:验证码用完就删(del self.codes[username]),防止重复使用;用户数据存在 CSV 文件里,重启程序不会丢。
项目 3:15 分钟 - Flask Web 版登录注册系统
组合前面的能力,做一个真正的 Web 应用。
# 需要先安装:pip install flask
from flask import Flask, request, make_response, redirect, url_for, render_template_string
app = Flask(__name__)
# 简化版:内存存储(生产环境用数据库)
sessions = {}
users = {'admin': 'e10adc3949ba59abbe56e057f20f883e'} # 密码是 123456
@app.route('/')
def index():
"""首页"""
session_id = request.cookies.get('session_id')
username = sessions.get(session_id, {}).get('username') if session_id else None
if username:
return f'''
<h1>欢迎回来,{username}!</h1>
<p><a href="/logout">退出登录</a></p>
'''
return '''
<h1>欢迎访问</h1>
<p><a href="/login">登录</a> | <a href="/register">注册</a></p>
'''
@app.route('/login', methods=['GET', 'POST'])
def login():
"""登录页面"""
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
import hashlib
password_hash = hashlib.md5(password.encode()).hexdigest()
if username in users and users[username] == password_hash:
# 登录成功,创建 Session
import secrets
session_id = secrets.token_urlsafe(32)
sessions[session_id] = {'username': username}
resp = make_response(redirect(url_for('index')))
resp.set_cookie('session_id', session_id, max_age=3600, httponly=True)
return resp
return render_template_string(LOGIN_HTML, error='用户名或密码错误')
return render_template_string(LOGIN_HTML)
@app.route('/register', methods=['GET', 'POST'])
def register():
"""注册页面"""
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username in users:
return render_template_string(REGISTER_HTML, error='用户名已存在')
import hashlib
users[username] = hashlib.md5(password.encode()).hexdigest()
return redirect(url_for('login'))
return render_template_string(REGISTER_HTML)
@app.route('/logout')
def logout():
"""登出"""
session_id = request.cookies.get('session_id')
if session_id and session_id in sessions:
del sessions[session_id]
resp = make_response(redirect(url_for('index')))
resp.delete_cookie('session_id')
return resp
# HTML 模板
LOGIN_HTML = '''
<!DOCTYPE html>
<html>
<head><title>登录</title></head>
<body>
<h1>登录</h1>
{% if error %}<p style="color:red">{{ error }}</p>{% endif %}
<form method="post">
<p><input name="username" placeholder="用户名" required></p>
<p><input name="password" type="password" placeholder="密码" required></p>
<p><button type="submit">登录</button></p>
</form>
<p><a href="/register">没有账号?去注册</a></p>
</body>
</html>
'''
REGISTER_HTML = '''
<!DOCTYPE html>
<html>
<head><title>注册</title></head>
<body>
<h1>注册</h1>
{% if error %}<p style="color:red">{{ error }}</p>{% endif %}
<form method="post">
<p><input name="username" placeholder="用户名" required></p>
<p><input name="password" type="password" placeholder="密码" required></p>
<p><button type="submit">注册</button></p>
</form>
<p><a href="/login">已有账号?去登录</a></p>
</body>
</html>
'''
if __name__ == '__main__':
print("启动登录注册系统...")
print("访问 http://127.0.0.1:5000")
app.run(debug=True, port=5000)
运行方式:保存为 app.py,运行 python app.py,然后:
1. 打开浏览器访问 http://127.0.0.1:5000
2. 点击"注册",创建新用户
3. 用新用户登录
4. 关掉浏览器再打开,发现还是登录状态(因为 Cookie 还在)
5. 点击"退出登录",清除登录状态
预期效果:可以看到浏览器 Cookie 中存储了 session_id,登录状态在多次请求间保持。
一句话解释:Flask 帮你处理了 HTTP 请求和 Cookie,你只需要关心业务逻辑——注册存用户、登录验证、登出删 Session。
💪 进阶:5 个新手必踩的坑 + 调试技巧
坑 1:密码明文存储 ❌ → 哈希存储 ✅
# ❌ 错误:直接存明文密码
users['小明'] = '123456'
# ✅ 正确:存密码的哈希
import hashlib
users['小明'] = hashlib.sha256('123456'.encode()).hexdigest()
为什么:如果数据库泄露,明文密码会导致用户在其他网站也被盗(很多人重复使用密码)。
坑 2:Session ID 可预测 ❌ → 用安全随机数 ✅
# ❌ 错误:用时间戳做 Session ID
session_id = str(time.time())
# ✅ 正确:用加密安全的随机数
import secrets
session_id = secrets.token_urlsafe(32)
为什么:如果 Session ID 可预测,攻击者可以直接算出来然后伪造会话。
坑 3:Cookie 没有设置 HttpOnly ❌ → 设置 HttpOnly ✅
# ❌ 错误:没有设置安全标志
resp.set_cookie('session_id', session_id)
# ✅ 正确:设置 HttpOnly 防止 XSS 读取
resp.set_cookie('session_id', session_id, httponly=True, secure=True)
为什么:HttpOnly 让 JavaScript 无法读取 Cookie,防止 XSS 攻击偷走会话。
坑 4:验证码不设置过期时间 ❌ → 设置过期时间 ✅
# ❌ 错误:验证码永不过期
self.codes[username] = code
# ✅ 正确:设置过期时间
import time
self.codes[username] = {
'code': code,
'expires_at': time.time() + 300 # 5分钟后过期
}
为什么:验证码如果不过期,攻击者可以慢慢暴力猜,设置短有效期更安全。
坑 5:Session 存在内存不持久化 ❌ → 用数据库/Redis ✅
# ❌ 错误:Session 存内存(重启丢失)
self.sessions = {}
# ✅ 正确:用文件或数据库持久化
# 生产环境推荐 Redis
为什么:内存存储的 Session 重启后就丢了,用户被迫全部登出。Redis 既快又支持持久化。
调试技巧:print 大法 + 日志
import logging
# 设置日志
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def login(username, password):
logger.debug(f"尝试登录: {username}")
if username not in users:
logger.warning(f"登录失败-用户不存在: {username}")
return False
# ... 验证逻辑 ...
logger.info(f"登录成功: {username}")
return True
运行后可以看到:
DEBUG:__main__:尝试登录: 张三
WARNING:__main__:登录失败-用户不存在: 张三
INFO:__main__:登录成功: 李四
为什么比 print 好:可以控制级别、输出到文件、格式更规范。
✏️ 练习题
练习 1(2 分钟):换个密码登录
- 输入:修改项目 1 的
SimpleAuth类,用密码admin123注册并登录 - 预期输出:注册成功,登录成功
- 提示:改
SimpleAuth().register('admin', 'admin123')里的密码参数
练习 2(3 分钟):加个登录次数限制
- 输入:在
SimpleAuth.login()方法里,加一个判断,如果用户名不存在就打印"第 X 次尝试" - 预期输出:连续调用 3 次 login('不存在用户', '任意密码'),输出"第1次尝试"、"第2次尝试"、"第3次尝试"
- 提示:用
enumerate或手动计数器
练习 3(5 分钟):用 CSV 处理新用户
- 输入:手动创建一个
new_users.csv文件,内容:
username,password_hash
王五,e10adc3949ba59abbe56e057f20f883e
赵六,5f4dcc3b5aa765d61d8327deb882cf99
用 CompleteAuth._load_users() 读取,打印所有用户名
- 预期输出:
用户列表: ['admin', '王五', '赵六']
- 提示:
csv.DictReader读取后取row['username']
练习 4(5 分钟):串接登录和 Session 检查
- 输入:用项目 2 的
CompleteAuth,登录后用返回的 session_id 调用get_logged_in_user() - 预期输出:返回登录的用户名
- 提示:
success, session_id, _ = auth.login(...)然后auth.get_logged_in_user(session_id)
练习 5(5 分钟):分析报错
- 输入:以下代码运行后报错
KeyError: 'username',找出原因并修复
session = auth.check_session('不存在的session_id')
print(session['username']) # 这行报错
- 预期输出:修复后应该打印
None而不是报错 - 提示:先检查 session 是否为 None
作业:做一个完整的登录注册系统
需求描述:用 Flask 做一个带以下功能的登录注册系统
功能点:
1. 用户注册:用户名、密码(密码要用 SHA256 哈希存储)
2. 用户登录:验证用户名密码,正确则创建 Session
3. 登录状态保持:用 Cookie 记住登录状态,关闭浏览器后再打开还能保持登录
4. 查看个人信息:登录后可以看到自己的用户名和注册时间
5. 退出登录:清除 Session 和 Cookie
加分项:
1. 验证码注册:注册前要先输入邮箱收到的验证码
2. 登录日志:记录每次登录的时间、IP(用 request.remote_addr)
验收标准:
- 能跑起来:python app.py 后访问 http://127.0.0.1:5000 无报错
- 注册新用户后,可以用新用户登录
- 登录后刷新页面还是登录状态
- 点"退出"后再访问变成未登录状态
提交方式:把代码保存为 app.py,在评论区贴上代码或 GitHub 链接
📚 总结 + 资源
本文学了 3 个核心点:
1. Session = 服务器端的"临时户口本",存着谁在访问
2. Cookie = 浏览器端的"会员卡",带着 Session ID 来证明身份
3. 登录注册流程 = 注册创建用户,登录验证密码+创建 Session,登出删除 Session
延伸学习资源:
- Flask 官方文档 - Session - 官方出品,详细讲解 Flask 的 Session 机制
- Python hashlib 文档 - 官方文档,了解更多哈希算法
- 《Python Web 开发实战》- 董伟明著 - 国内最好的 Flask 实战书,第 6 章讲认证和权限
互动钩子:你在实际项目中用过 Session 和 Cookie 吗?有没有遇到过什么奇葩的登录 bug?评论区聊聊,老粉优先回复!
下一章我们要进入数据库的世界——所有用户数据最终都要存在数据库里,Session 也是。你准备好了吗?

评论(0)