Flask-Login 实战:用户登录、登出与 Session 管理

📂 所属阶段:第三阶段 — 用户系统(安全篇)
🔗 相关章节:密码安全加密 · 数据验证


1. Flask-Login 概述

1.1 核心功能

Flask-Login 提供:
1. 用户登录 / 登出
2. Session 管理(Cookie)
3. 登录保护(login_required)
4. 用户加载回调(user_loader)
5. 当前用户访问(current_user)

1.2 安装

pip install flask-login

2. 配置

2.1 初始化

# app/extensions.py
from flask_login import LoginManager

login_manager = LoginManager()

# 登录视图(未登录时重定向到这里)
login_manager.login_view = "auth.login"
login_manager.login_message = "请先登录后再访问"
login_manager.login_message_category = "warning"

2.2 应用工厂中初始化

# app/__init__.py
from app.extensions import db, login_manager

def create_app():
    app = Flask(__name__)
    # ...其他配置...
    db.init_app(app)
    login_manager.init_app(app)

    # 用户加载回调(必须!)
    @login_manager.user_loader
    def load_user(user_id):
        from app.models import User
        return db.session.get(User, int(user_id))

    return app

2.3 用户模型必须实现的方法

# app/models/user.py
from flask_login import UserMixin
from app.extensions import db

class User(UserMixin, db.Model):
    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(120), unique=True, nullable=False)
    username = db.Column(db.String(50), unique=True)
    password_hash = db.Column(db.String(256), nullable=False)
    is_active = db.Column(db.Boolean, default=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    # Flask-Login 必需的 4 个属性/方法(UserMixin 已实现大部分)
    # is_authenticated: 是否已登录
    # is_active: 账户是否激活
    # is_anonymous: 是否是匿名用户
    # get_id(): 获取用户唯一标识符

    def __repr__(self):
        return f"<User {self.email}>"

3. 登录与登出路由

3.1 登录路由

# app/routes/auth.py
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, current_user
from werkzeug.security import check_password_hash
from app.extensions import db
from app.models import User
from app.forms import LoginForm

auth_bp = Blueprint("auth", __name__, url_prefix="/auth")

@auth_bp.route("/login", methods=["GET", "POST"])
def login():
    # 已登录用户访问登录页 → 跳转到首页
    if current_user.is_authenticated:
        return redirect(url_for("main.index"))

    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()

        if user and check_password_hash(user.password_hash, form.password.data):
            if not user.is_active:
                flash("账户已被禁用,请联系管理员。", "danger")
                return render_template("auth/login.html", form=form)

            # 登录成功
            login_user(user, remember=form.remember_me.data)

            # 记住来源页面(登录后跳转回上一页)
            next_page = request.args.get("next")
            if next_page and next_page.startswith("/"):
                return redirect(next_page)

            return redirect(url_for("main.index"))
        else:
            flash("邮箱或密码错误", "danger")

    return render_template("auth/login.html", form=form)

3.2 登出路由

# app/routes/auth.py
from flask_login import logout_user, login_required

@auth_bp.route("/logout")
@login_required
def logout():
    logout_user()  # 清除 session
    flash("已安全退出登录。", "info")
    return redirect(url_for("auth.login"))

4. 登录保护

4.1 login_required 装饰器

from flask_login import login_required

@auth_bp.route("/profile")
@login_required
def profile():
    return render_template("auth/profile.html", user=current_user)

@auth_bp.route("/settings", methods=["GET", "POST"])
@login_required
def settings():
    form = SettingsForm()
    if form.validate_on_submit():
        current_user.bio = form.bio.data
        db.session.commit()
        flash("资料已更新!", "success")
        return redirect(url_for("auth.profile"))
    return render_template("auth/settings.html", form=form)

4.2 局部登录保护

# 仅 POST 请求需要登录(GET 公开,POST 受保护)
@app.route("/comment", methods=["POST"])
@login_required
def add_comment():
    ...

4.3 自定义权限装饰器

# app/decorators.py
from functools import wraps
from flask import abort
from flask_login import current_user

def admin_required(f):
    """仅管理员可访问"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not current_user.is_authenticated or not current_user.is_admin:
            abort(403)  # Forbidden
        return f(*args, **kwargs)
    return decorated_function

def editor_required(f):
    """编辑者或管理员可访问"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not current_user.is_authenticated:
            abort(401)
        if not (current_user.is_admin or current_user.role == "editor"):
            abort(403)
        return f(*args, **kwargs)
    return decorated_function

# 使用
@auth_bp.route("/admin")
@admin_required
def admin_panel():
    return "管理后台"

5. Session 配置

# app/__init__.py
app.config["SECRET_KEY"] = "your-secret-key-change-in-production"
app.config["REMEMBER_COOKIE_DURATION"] = timedelta(days=14)  # "记住我"Cookie 有效期
app.config["SESSION_COOKIE_SECURE"] = True    # 仅 HTTPS 传输(生产)
app.config["SESSION_COOKIE_HTTPONLY"] = True  # JS 无法读取 Cookie
app.config["SESSION_COOKIE_SAMESITE"] = "Lax" # CSRF 防护

6. 模板中的 current_user

<!-- templates/base.html -->

<!-- 导航栏根据登录状态显示不同内容 -->
<nav>
    <a href="{{ url_for('index') }}">首页</a>

    {% if current_user.is_authenticated %}
        <a href="{{ url_for('profile') }}">{{ current_user.username }}</a>
        {% if current_user.is_admin %}
            <a href="{{ url_for('admin.index') }}">管理后台</a>
        {% endif %}
        <a href="{{ url_for('logout') }}">退出</a>
    {% else %}
        <a href="{{ url_for('login') }}">登录</a>
        <a href="{{ url_for('register') }}">注册</a>
    {% endif %}
</nav>

7. 小结

# Flask-Login 速查

# 初始化
login_manager.login_view = "auth.login"

# 用户加载
@login_manager.user_loader
def load_user(user_id):
    return db.session.get(User, int(user_id))

# 用户模型继承
class User(UserMixin, db.Model): ...

# 登录登出
login_user(user, remember=False)
logout_user()

# 当前用户
current_user.is_authenticated  # 是否登录
current_user.id                # 用户 ID

# 保护路由
@login_required
def protected(): ...

# 登出后重定向(自动由 login_manager 处理)

💡 安全提示SECRET_KEY 必须足够随机且保密,建议使用 secrets.token_hex(32) 生成并存储在环境变量中,永不上传代码仓库。


🔗 扩展阅读