第4章 4.5 综合实战:登录注册系统

🎯 开场:为什么你每次打开网站都要"登录"?

上一章我们学会了怎么用 Python 发送邮件、生成验证码。但是发完验证码就结束了吗? 当然没有——用户输入验证码后,你的网站还得"记住"他是谁,接下来几个小时甚至几天,他再访问网站都不用重新输入密码。

这个"记住用户"的能力,就是今天要解决的核心问题。

你有没有遇到过这种情况:
- 在网吧登录了邮箱,走的时候没点"退出",结果下个人直接登上你的邮箱了
- 网站提示"记住我",勾选了之后一个月不用登录,没勾选关掉浏览器就得重来

这背后的原理,就是 Session 和 Cookie。 今天我们要用 Python 做一个完整的登录注册系统,包含:
- 验证码校验(接上章)
- Session 管理(服务器端记住用户)
- Cookie 机制(浏览器端保存登录状态)
- 完整的注册→登录→登出流程

学完这章,你就能写出一个真正能用的用户系统了。


🧱 基础:三个核心概念,像钥匙、门禁卡和登记簿

1. Sessio\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\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

延伸学习资源

  1. Flask 官方文档 - Session - 官方出品,详细讲解 Flask 的 Session 机制
  2. Python hashlib 文档 - 官方文档,了解更多哈希算法
  3. 《Python Web 开发实战》- 董伟明著 - 国内最好的 Flask 实战书,第 6 章讲认证和权限

互动钩子:你在实际项目中用过 Session 和 Cookie 吗?有没有遇到过什么奇葩的登录 bug?评论区聊聊,老粉优先回复!


下一章我们要进入数据库的世界——所有用户数据最终都要存在数据库里,Session 也是。你准备好了吗?

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。