零基础入门python23:Flask-Login登录、退出与会话

零基础入门python23:Flask-Login登录、退出与会话

一、上一篇课后练习讲解

邮箱大小写测试应先用 Alice@Example.com 注册,再用 alice@example.com 登录,并断言数据库只有一条用户记录。归一化应发生在查询和写入之前。

上一篇课后练习完整答案

上一篇练习的要求已落实到下面完整文件;先运行项目测试,再用 curl 对照状态码和数据库持久化结果

答案要点:注册先规范化邮箱和校验密码,再生成带盐哈希;唯一约束冲突捕获 IntegrityError 后 rollback 并返回 409。

文件:app/auth.py

完整参考答案文件

完整文件:app/auth.py

python 复制代码
from flask import Blueprint, request
from sqlalchemy.exc import IntegrityError
from werkzeug.security import generate_password_hash
from .extensions import db
from .models import User
bp = Blueprint("auth", __name__, url_prefix="/api/auth")
@bp.post("/register")
def register():
    data = request.get_json(silent=True) or {}
    email = str(data.get("email", "")).strip().casefold()
    password = str(data.get("password", ""))
    if "@" not in email or len(password) < 8:
        return {"error": "input_invalid"}, 400
    db.session.add(User(email=email, password_hash=generate_password_hash(password)))
    try:
        db.session.commit()
    except IntegrityError:
        db.session.rollback()
        return {"error": "email_exists"}, 409
    return {"email": email}, 201

完整参考答案文件

本篇对应的交付源码完整文件:flask-ledger/app/auth.py

python 复制代码
from flask import Blueprint, request
from flask_login import current_user, login_required, login_user, logout_user

from .extensions import db
from .models import User

bp = Blueprint("auth", __name__, url_prefix="/api/auth")


@bp.post("/register")
def register():
    data = request.get_json(silent=True) or {}
    email = str(data.get("email", "")).strip().lower()
    password = str(data.get("password", ""))
    if "@" not in email:
        return {"message": "邮箱格式不正确"}, 400
    if len(password) < 8:
        return {"message": "密码至少8位"}, 400
    if db.session.scalar(db.select(User).where(User.email == email)):
        return {"message": "邮箱已注册"}, 409
    user = User(email=email)
    user.set_password(password)
    db.session.add(user)
    db.session.commit()
    return {"id": user.id, "email": user.email}, 201


@bp.post("/login")
def login():
    data = request.get_json(silent=True) or {}
    email = str(data.get("email", "")).strip().lower()
    user = db.session.scalar(db.select(User).where(User.email == email))
    if not user or not user.check_password(str(data.get("password", ""))):
        return {"message": "邮箱或密码错误"}, 401
    login_user(user)
    return {"id": user.id, "email": user.email}


@bp.post("/logout")
@login_required
def logout():
    logout_user()
    return {"message": "已退出"}


@bp.get("/me")
@login_required
def me():
    return {"id": current_user.id, "email": current_user.email}

验收命令:python -m pytest -q(Django 项目使用 python manage.py test)。预期测试通过;若失败先检查迁移、配置和事务回滚。

二、本篇完成什么

让注册用户登录并保持会话,完成 /me 查看当前用户和 /logout 退出。登录状态由 Flask-Login 从 session 恢复,受保护接口使用 @login_required

三、为什么不用客户端传 user_id

/mecurrent_user.id 读取身份,而不是接受 JSON 中的 user_id。客户端提供的 id 只是普通输入,不能作为授权依据;身份必须来自已经验证的 session。

四、验收流程

powershell 复制代码
curl -c cookies.txt -X POST http://127.0.0.1:5000/api/auth/login -H "Content-Type: application/json" -d "{\"email\":\"alice@example.com\",\"password\":\"secret123\"}"
curl -b cookies.txt http://127.0.0.1:5000/api/auth/me
curl -b cookies.txt -X POST http://127.0.0.1:5000/api/auth/logout
curl -b cookies.txt http://127.0.0.1:5000/api/auth/me

预期登录和 /me 为 200,退出后 /me 为 401。测试必须覆盖错误密码、未登录访问和退出后会话失效。课后练习:增加会话过期配置,并解释开发环境和生产环境的 Cookie 安全属性差异。

项目增量:Flask-Login 会话边界

登录成功后使用安全 cookie 保存会话标识,服务器通过 user_loader 从数据库加载用户;注销必须清除会话,禁用用户不能继续访问。

python 复制代码
@login_manager.user_loader
def load_user(user_id):
    return db.session.get(User, int(user_id))

@auth_bp.post('/login')
def login():
    user = User.query.filter_by(email=request.form['email'].strip().casefold()).first()
    if user is None or not user.check_password(request.form['password']):
        abort(401)
    login_user(user)
    return redirect(url_for('ledger.index'))

验收未登录访问账目返回 401 或重定向,登录后只能看到自己的数据;课后练习测试 session cookie 和禁用用户。

五、会话到底保存了什么

Flask-Login 默认在 cookie 中保存一个不可直接信任的用户标识,服务端每次请求通过 user_loader 查询用户。cookie 被篡改时签名校验会失败,current_user 变成匿名用户;因此不能把"是否登录"交给前端传来的 user_id

python 复制代码
@login_manager.user_loader
def load_user(user_id: str):
    # 统一从数据库加载,找不到用户时返回 None,框架会按匿名用户处理。
    try:
        return db.session.get(User, int(user_id))
    except (TypeError, ValueError):
        return None

@login_manager.unauthorized_handler
def unauthorized():
    # API 客户端需要 JSON 和 401,而不是浏览器登录页的 302 跳转。
    return {"error": "login_required"}, 401

登录接口不应区分"邮箱不存在"和"密码错误",否则攻击者可以枚举注册用户。remember 会生成更长寿命的 cookie,只有在用户明确勾选时才启用。若项目改成前后端分离并使用 JWT,仍然需要保留同样的认证边界,只是令牌存储位置不同。

python 复制代码
def test_login_session(client, user):
    response = client.post("/api/auth/login", json={
        "email": user.email, "password": "correct-horse"
    })
    assert response.status_code == 200
    me = client.get("/api/auth/me")
    assert me.status_code == 200
    assert me.json["id"] == user.id
    client.post("/api/auth/logout")
    assert client.get("/api/auth/me").status_code == 401

测试中必须复用同一个 client,因为 cookie 存在客户端对象里。出现"明明登录了仍是 401"时,检查 SECRET_KEY 是否在每次创建应用时稳定,以及 user_loader 是否把字符串 id 转成整数;如果每次请求都创建新 client,会话当然不会保留。

七、禁用用户和会话失效

User 增加 is_active 字段后,is_authenticated 仍然可以为真,但 is_active 应在 user_loader 中检查。管理员禁用用户后,下一次请求应立即失效,而不是等 cookie 自然过期:

python 复制代码
@login_manager.user_loader
def load_user(user_id):
    user = db.session.get(User, int(user_id))
    return user if user and user.is_active else None

八、上一篇练习讲解与本篇练习

上一篇的并发注册测试应使用两个独立 Session;本篇练习增加 GET /api/auth/me 和禁用用户测试,要求退出后再次访问返回 401。下一篇会在这个身份基础上实现分类 CRUD,并演示越权访问必须返回 404 还是 403。

九、Cookie 安全实验

在浏览器开发者工具的 Application → Cookies 中确认会话 cookie 具有 HttpOnlySameSite=Lax;若线上使用 HTTPS,再打开 Secure。把 cookie 的值改一个字符后重新访问 /api/auth/me,应得到 401,说明签名校验生效。不要把 Flask 的签名 cookie 当作"加密数据库",其中内容仍可能被读取;敏感资料应放服务端 Session 或数据库。

如果采用前后端分离,跨域请求还要正确设置 CORS 的 supports_credentials,并限制允许的 origin;不能写 * 同时携带 cookie。完成实验后清理浏览器 cookie,避免旧会话干扰下一次测试。

本篇结束:完整模块文件

下面是交付项目中真实存在的完整文件 flask-ledger/app/auth.py。它覆盖本篇新增逻辑以及前文已经完成的依赖代码;复制单个函数会丢失上下文,因此这里提供整份文件。

python 复制代码
from flask import Blueprint, request
from flask_login import current_user, login_required, login_user, logout_user

from .extensions import db
from .models import User

bp = Blueprint("auth", __name__, url_prefix="/api/auth")


@bp.post("/register")
def register():
    data = request.get_json(silent=True) or {}
    email = str(data.get("email", "")).strip().lower()
    password = str(data.get("password", ""))
    if "@" not in email:
        return {"message": "邮箱格式不正确"}, 400
    if len(password) < 8:
        return {"message": "密码至少8位"}, 400
    if db.session.scalar(db.select(User).where(User.email == email)):
        return {"message": "邮箱已注册"}, 409
    user = User(email=email)
    user.set_password(password)
    db.session.add(user)
    db.session.commit()
    return {"id": user.id, "email": user.email}, 201


@bp.post("/login")
def login():
    data = request.get_json(silent=True) or {}
    email = str(data.get("email", "")).strip().lower()
    user = db.session.scalar(db.select(User).where(User.email == email))
    if not user or not user.check_password(str(data.get("password", ""))):
        return {"message": "邮箱或密码错误"}, 401
    login_user(user)
    return {"id": user.id, "email": user.email}


@bp.post("/logout")
@login_required
def logout():
    logout_user()
    return {"message": "已退出"}


@bp.get("/me")
@login_required
def me():
    return {"id": current_user.id, "email": current_user.email}
相关推荐
微小冷1 小时前
Python图论库NetworkX初步
开发语言·python·图论·graph·networkx·digraph
Data_Journal2 小时前
如何使用 Python 抓取 Google Flights:分步指南
大数据·开发语言·数据库·python·scrapy
你我一见如故2 小时前
仿QQ音乐桌面客户端——测试报告
python·selenium
Kismet_nvi2 小时前
Python 基础核心知识点总结
开发语言·windows·python
2401_844582953 小时前
冷门技巧:AI模型可靠性测试的实用技巧:数
python
you鬰3 小时前
datawhale--llm-algo-leetcode5️⃣
python·datawhale
λqaq73 小时前
MySQL 数据库基础(2)约束、用户权限、远程连接、外键与 TCP/UDP 理解
linux·数据库·python·tcp/ip·mysql
人工干智能3 小时前
科普:pandas 时间偏移别名:ts.resample(“5Min“).sum()
python·pandas
开源量化GO3 小时前
最新AI辅助量化表达:先理清规则,再按需求选工具
人工智能·python