Flask-Login — ユーザー登録、ログイン、ログアウト

この章では、Flask-Login を使用して Session ユーザー認証を実装する方法を学びます。これは Flask Web 開発において最も重要なセキュリティ対策です。


Flask-Login とは?

Web アプリケーションの HTTP リクエストは本質的にステートレスです。サーバーはあなたが誰であるかを自動的に記憶しません。

Flask-LoginによってSessionメカニズムがユーザー状態の維持を実現します:ログイン後、サーバーは Session にユーザー ID を保存し、後続のリクエストは現在のユーザーを自動的に識別します。


インストールと初期化

(venv) $ pip install flask-login

例

# ファイルパス: app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
app.config['SECRET_KEY'] = 'your-secret-key-here'   # Session 暗号化キー(本番環境では環境変数を使用)

db = SQLAlchemy(app)

# LoginManager を初期化
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'auth.login'     # 未ログインユーザーはログインページにリダイレクト
login_manager.login_message = '先にログインしてからアクセスしてください。'

SECRET_KEYFlask セキュリティ体系の基盤です。Session 暗号化、CSRF Token、Flash メッセージはすべてこれに依存します。本番環境では環境変数から読み取る必要があり、ハードコーディングしてはなりません。


User モデルを定義する

Flask-Login は User モデルに実装を要求しますUserMixin提供されるいくつかのメソッド。

例

# ファイルパス: models.py に新規追加
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash

class User(UserMixin, db.Model):
    """ユーザーモデル"""
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(256), nullable=False)

    def set_password(self, password):
        """パスワードをハッシュ暗号化する(平文パスワードは決して保存しない)"""
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        """パスワードが一致するか検証する"""
        return check_password_hash(self.password_hash, password)

    def __repr__(self):
        return f'<User {self.username}>'

# user_loader を定義:Flask-Login はこの関数を使用して session からユーザーオブジェクトを復元します
@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

UserMixin は User モデルに以下の属性とメソッドを自動的に注入します:

属性/メソッド説明
is_authenticatedユーザーがログインしているかどうか
is_activeユーザーがアクティブ状態かどうか
is_anonymous匿名ユーザー(未ログイン)かどうか
get_id()ユーザーの主キー ID を取得する

マイグレーションを生成して実行する:

(venv) $ flask db migrate -m "新增 User 模型"
(venv) $ flask db upgrade

認証 Blueprint を作成する

例

# ファイルパス: app/blueprints/auth.py
from flask import Blueprint, render_template, redirect, url_for, request, flash
from flask_login import login_user, logout_user, login_required, current_user
from app.models import User, db

auth_bp = Blueprint('auth', __name__)

@auth_bp.route("/register", methods=['GET', 'POST'])
def register():
    """ユーザー登録"""
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))

    if request.method == 'POST':
        username = request.form.get('username', '').strip()
        email = request.form.get('email', '').strip()
        password = request.form.get('password', '')

        # 必須フィールドを検証
        if not username or not email or not password:
            flash('すべてのフィールドに入力する必要があります。', 'error')
            return render_template('register.html')

        # ユーザー名とメールアドレスが既に存在するか確認
        if User.query.filter_by(username=username).first():
            flash('このユーザー名は既に使用されています。', 'error')
            return render_template('register.html')
        if User.query.filter_by(email=email).first():
            flash('このメールアドレスは既に登録されています。', 'error')
            return render_template('register.html')

        # ユーザーを作成
        user = User(username=username, email=email)
        user.set_password(password)    # パスワードを暗号化して保存
        db.session.add(user)
        db.session.commit()

        login_user(user)               # 登録後に自動ログイン
        flash(f'登録成功、ようこそ、{username}さん!', 'success')
        return redirect(url_for('main.index'))

    return render_template('register.html')


@auth_bp.route("/login", methods=['GET', 'POST'])
def login():
    """ユーザーログイン"""
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))

    if request.method == 'POST':
        username = request.form.get('username', '').strip()
        password = request.form.get('password', '')

        # user_loader がデータベースからユーザーを検索
        user = User.query.filter_by(username=username).first()

        # パスワードを検証(パスワード自体はログに記録されません)
        if user is None or not user.check_password(password):
            flash('ユーザー名またはパスワードが正しくありません。', 'error')
            return render_template('login.html')

        login_user(user, remember=request.form.get('remember'))
        flash(f'おかえりなさい、{username}さん!', 'success')

        # ログイン後、ログイン前にアクセスしていたページにリダイレクト(URL の next パラメータ)
        next_page = request.args.get('next')
        return redirect(next_page or url_for('main.index'))

    return render_template('login.html')


@auth_bp.route("/logout")
@login_required       # ログインユーザーのみログアウトを実行できます
def logout():
    logout_user()
    flash('安全にログアウトしました。', 'info')
    return redirect(url_for('main.index'))

App 設計で必要な参照

例

# ファイルパス: app.py で auth Blueprint を登録
from app.blueprints.auth import auth_bp
app.register_blueprint(auth_bp)

パスワードを平文で保存してはいけません!generate_password_hash()PBKDF2 アルゴリズムを使用してパスワードを salted hash にします。データベースが漏洩しても、攻撃者はパスワードを逆算できません。パスワードを検証するときはcheck_password_hash()。


ログインと登録のテンプレートを作成する

例

<!-- ファイルパス: app/templates/login.html -->
{% extends 'base.html' %}

{% block title %}ログイン - サンプル ブログ{% endblock %}

{% block content %}
<div class="auth-form">
    <h2>ログイン</h2>
    <form method="post">
        <div class="form-group">
            <label>ユーザー名</label>
            <input type="text" name="username" required>
        </div>
        <div class="form-group">
            <label>パスワード</label>
            <input type="password" name="password" required>
        </div>
        <div class="form-group">
            <label>
                <input type="checkbox" name="remember">ログイン状態を保持する
            </label>
        </div>
        <button type="submit" class="btn-submit">ログイン</button>
    </form>
    <p class="form-footer">
アカウントをお持ちでないですか?<a href="{{ url_for('auth.register') }}">今すぐ登録</a>
    </p>
</div>
{% endblock %}

ナビゲーションバーを更新してログイン状態を表示する

例

<!-- ファイルパス: app/templates/base.html ナビゲーションバー部分 -->
<nav>
    <a href="/">ホーム</a>
    {% if current_user.is_authenticated %}
        <span class="user-name">{{ current_user.username }}</span>
        <a href="{{ url_for('auth.logout') }}">ログアウト</a>
    {% else %}
        <a href="{{ url_for('auth.login') }}">ログイン</a>
        <a href="{{ url_for('auth.register') }}">登録</a>
    {% endif %}
</nav>

current_userFlask-Login が自動的に注入するプロキシオブジェクトで、すべてのテンプレートとビュー関数で直接使用できます。ログイン状態では User オブジェクト、未ログイン時は AnonymousUserMixin インスタンスです。


本章のまとめ

この章では Flask-Login の Session 認証を導入しました。UserMixin + user_loader でユーザーモデルを構成、login_user/logout_user で Session を管理、werkzeug でパスワードをハッシュ化、@login_required でビューを保護、テンプレート内で current_user を使って状態を判定します。

これでブログに完全なユーザー登録・ログイン・ログアウトのフローが備わりました。

その他の拡張