Flask-Login practice: user login, logout and session management

📂 Stage: Stage 3 - User System (Security) 🔗 Related chapters: 密码安全加密 · 数据验证

1. Why choose Flask-Login?

Manually managing user sessions sounds easy, but when you actually implement it, you will encounter a series of pitfalls: cookie hijacking protection, automatic redirection without logging in, persistence of the "remember me" function, acquisition of the current user across requests... If you write these logics from scratch, you will reinvent the wheel at best, and bury security risks at worst.

Flask-Login has encapsulated all these common logics. You only need to complete two things:

  • Implement User loading callback - tell Flask-Login how to find the user object based on ID;
  • Provides login/logout routing - call thelogin_user()andlogout_user()That’s it.

The rest of the session management,current_useracting,@login_requiredProtect, remember me cookies, and more, Flask-Login has it all.

Core Competencies at a Glance

  • ✅Creation, destruction and automatic maintenance of user Session
  • ✅ Globally availablecurrent_user, the current user can be directly obtained in the view and template
  • @login_requiredDecorator, one line of code to protect routing
  • ✅ Automatic cookie management for the "Remember Me" function (configurable validity period)
  • ✅ Unified redirection and message prompts when you are not logged in or have no permissions
  • ✅ Built-in implementation of anonymous users, no need to write anonymous objects yourself

Install

pip install flask-login

2. Global initialization and basic configuration

Flask recommends using the Application Factory Pattern to create application instances, which makes it more convenient to manage configurations in multiple environments (development, testing, production). For this reason, we need to separate the initialization of third-party extensions to avoid circular imports.

2.1 Create extension manager

Create a new oneapp/extensions.pyFile, specifically used to store instantiations of extensions such as Flask-Login:

# 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"   # 消息的分类(适配 Bootstrap 风格)

Here will belogin_viewpoint toauthblueprintloginRouting so that when a user is not logged in and accesses a protected page, they are automatically redirected to the login page.login_messageIt will be stored in the flash message for template rendering.

2.2 Bind the extension in the application factory

Next, bind the extension in the application factory function and implement the core user loading callback:

# app/__init__.py
from flask import Flask
from datetime import timedelta
from app.extensions import db, login_manager
from app.routes import auth_bp, main_bp

def create_app(config_class=None):
    app = Flask(__name__)

    # 安全相关配置(生产环境务必使用环境变量!)
    app.config["SECRET_KEY"] = "dev-only-secret-key-please-change-this"
    app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

    # “记住我”与 Session 安全设置
    app.config["REMEMBER_COOKIE_DURATION"] = timedelta(days=14)  # 记住我Cookie有效期14天
    app.config["SESSION_COOKIE_SECURE"] = False   # 开发环境False,生产环境必须True(仅HTTPS)
    app.config["SESSION_COOKIE_HTTPONLY"] = True  # 禁止JavaScript读取Cookie,防XSS
    app.config["SESSION_COOKIE_SAMESITE"] = "Lax" # 基础CSRF防护

    # 绑定扩展到当前应用
    db.init_app(app)
    login_manager.init_app(app)

    # 用户加载回调:每次请求都会根据Session中的user_id调用此函数
    @login_manager.user_loader
    def load_user(user_id):
        from app.models.user import User
        # 使用 SQLAlchemy 2.0 推荐的 Session.get() 方法,更简洁高效
        return db.session.get(User, int(user_id))

    # 注册蓝图
    app.register_blueprint(main_bp)
    app.register_blueprint(auth_bp, url_prefix="/auth")

    return app

Tip:SECRET_KEYBe sure to read it from the environment variables during deployment and ensure it is random and confidential. It must not be hard-coded in the code and submitted to the repository.

3. User model: inheritanceUserMixin

Flask-Login requires user objects to implement four core properties/methods:

  1. is_authenticated– Are you logged in?
  2. is_active– Whether the account is activated (can be disabled)
  3. is_anonymous– Whether it is an anonymous user
  4. get_id()– Returns the user’s unique identifier (string or number)

In order to prevent yourself from implementing these methods one by one, directly inheritUserMixinThat’s it. If there is a business need, you can also override one of the attributes (such asis_active)。

# app/models/user.py
from datetime import datetime
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, index=True)
    username = db.Column(db.String(50), unique=True, index=True)
    password_hash = db.Column(db.String(256), nullable=False)
    is_active = db.Column(db.Boolean, default=True)   # 可用于封禁账户
    is_admin = db.Column(db.Boolean, default=False)   # 自定义的权限字段
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

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

In this way, the user model meets all the requirements of Flask-Login and has database mapping capabilities.

4. Core routing: login and logout

4.1 Login routing

The login route is responsible for handling form submission, password verification and session creation. The following uses the Flask-WTF form as an example. You can also use it directlyrequest.formGet data.

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

auth_bp = Blueprint("auth", __name__, template_folder="templates")

@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)

            # 登录核心:创建用户会话,remember 参数控制“记住我”
            login_user(user, remember=form.remember_me.data)

            # 处理登录后重定向:只允许跳转到相对路径,防止 open redirect 攻击
            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")

    # GET 请求或表单验证失败时渲染登录页面
    return render_template("auth/login.html", form=form)

A few key points:

  • check_password_hashUsed to compare the password entered by the user with the hash value stored in the database to achieve secure password verification.
  • login_user(user, remember=False)Create a login session; whenremember=TrueWhen , a long-term "remember me" cookie will be written, whose validity period is the previously configuredREMEMBER_COOKIE_DURATION
  • Prevent open redirect: only accept/The relative path at the beginning is used as a jump target to prevent attackers from using query parameters to redirect users to malicious websites.

4.2 Logout routing

Logout logic is very simple, one linelogout_user()This will clear the user session and "remember me" cookies.

@auth_bp.route("/logout")
@login_required   # 只有已登录用户才能执行登出操作
def logout():
    logout_user()
    flash("您已安全退出登录。", "info")
    return redirect(url_for("auth.login"))

5. Route protection—from basics to advanced

5.1 Basic usage:@login_required

Just add above the route@login_requiredDecorator to force login. When users who are not logged in access, they will be automatically redirected tologin_viewspecified address.

@auth_bp.route("/profile")
@login_required
def profile():
    # current_user 在视图和模板中均可直接使用
    return render_template("auth/profile.html", user=current_user)

Pay attention to the order of decorators: Flask's routing decorators are executed from bottom to top, so usually@login_requiredput on@app.routeabove (that is, the layer immediately next to the function definition).

5.2 Partial protection: decide whether to require login according to the request method

Some scenarios require more fine-grained control, such as "viewing comments is public, and you must log in to write comments." At this time, you can not use the decorator directly, but passcurrent_user.is_authenticatedDetermine inside the view.

from flask import Blueprint, request, redirect, url_for
from flask_login import login_required, current_user

main_bp = Blueprint("main", __name__)

@main_bp.route("/posts/<int:post_id>/comments", methods=["GET", "POST"])
def post_comments(post_id):
    if request.method == "POST":
        if not current_user.is_authenticated:
            # 将当前请求的URL作为 next 参数传递给登录页,登录后可以跳回
            return redirect(url_for("auth.login", next=request.url))
        # 处理添加评论……
    # 处理 GET 请求查看评论……

5.3 Custom permission decorator

Login is only the first threshold. In many cases, it is also necessary to verify whether the user has specific permissions, such as administrator access to the backend. We can write our own permission decorator:

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

def admin_required(f):
    """仅允许 is_admin=True 的用户访问"""
    @wraps(f)
    def decorated(*args, **kwargs):
        if not current_user.is_authenticated:
            abort(401)   # 未登录:401 Unauthorized
        if not current_user.is_admin:
            abort(403)   # 已登录但无权限:403 Forbidden
        return f(*args, **kwargs)
    return decorated

When using, place the custom decorator in@login_requiredAfter that (make sure to verify login first, then permissions):

@auth_bp.route("/admin")
@login_required
@admin_required
def admin_dashboard():
    return render_template("admin/dashboard.html")

6. Universal variables in templates:current_user

current_userNot only can it be used in view functions, but it is also globally available in Jinja2 templates. It couldn't be more convenient to use it to control the display of navigation bar and buttons.

<!-- templates/base.html -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
    <div class="container">
        <a class="navbar-brand" href="{{ url_for('main.index') }}">My Flask App</a>
        <div class="collapse navbar-collapse">
            <ul class="navbar-nav ms-auto">
                {% if current_user.is_authenticated %}
                    <!-- 已登录状态的导航 -->
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('auth.profile') }}">
                            {{ current_user.username or current_user.email }}
                        </a>
                    </li>
                    {% if current_user.is_admin %}
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('auth.admin_dashboard') }}">管理后台</a>
                        </li>
                    {% endif %}
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('auth.logout') }}">退出登录</a>
                    </li>
                {% else %}
                    <!-- 未登录状态的导航 -->
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('auth.login') }}">登录</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('auth.register') }}">注册</a>
                    </li>
                {% endif %}
            </ul>
        </div>
    </div>
</nav>

7. Safety practices cannot be ignored

Even though Flask-Login helps us encapsulate many details, there are still several security points that we need to check ourselves:

  1. SECRET_KEYIt must be strongly random and confidential used in production environmentsecrets.token_hex(32)Generate, inject via environment variables, never hardcode or commit to code repository.

  2. Prevent open redirect vulnerability Jump parameters after loginnextWhitelist verification is required, and only the/Starting with a relative path, external URLs are rejected.

  3. Force HTTPS and set secure cookie flag After deploying to production, be sure to setSESSION_COOKIE_SECURE=True, ensuring that session cookies are only transmitted in HTTPS connections to prevent man-in-the-middle attacks.

  4. Passwords should never be stored in clear text Must be used when registeringwerkzeug.security.generate_password_hash()Encrypt password and reuse it when logging incheck_password_hash()Compare.

  5. Decorator order matters The custom permission decorator should be placed in@login_requiredbelow (near the layer where the function is defined), make sure to verify the login status first, and then verify the specific permissions.

8. Quick summary

Finally, a list of commonly used Flask-Login operations is compiled for you for quick reference:

# --- 初始化与配置 ---
from flask_login import LoginManager
login_manager = LoginManager()
login_manager.login_view = "auth.login"

# --- 用户加载回调(必须实现) ---
@login_manager.user_loader
def load_user(user_id):
    return db.session.get(User, int(user_id))

# --- 用户模型 ---
from flask_login import UserMixin
class User(UserMixin, db.Model):
    # ... 字段定义

# --- 登录 / 登出 ---
login_user(user, remember=False)  # 记住我:remember=True
logout_user()

# --- 当前用户 ---
from flask_login import current_user
current_user.is_authenticated  # 是否登录
current_user.id                # 用户ID
current_user.username          # 自定义字段

# --- 路由保护 ---
@login_required
@admin_required  # 自定义装饰器
def protected():
    ...

🔗 Extended reading