Flaskでユーザー認証システム

2026-02-11

はじめに

Webアプリケーションにおいてユーザー認証は非常に重要な機能です。ユーザーが自分のアカウントを作成し、安全にログインして個人用のコンテンツにアクセスできるようにする仕組みは、現代のWebサービスには欠かせない要素となっています。Flaskを使用すると、比較的少ないコードで堅牢なユーザー認証システムを構築することができます。

この記事では、Flaskを使ったユーザー認証システムの構築方法について詳しく解説します。ユーザーの登録機能からログイン処理、パスワードの安全な管理方法、そしてセッションによる状態の保持まで、実際に動作するコード例を交えながら順を追って説明していきます。

認証システムを正しく実装することは、ユーザーデータを保護し、アプリケーションのセキュリティを確保するために不可欠です。特にパスワードの取り扱いには細心の注意が必要で、適切なハッシュ化技術の使用が求められます。Flaskの拡張機能を活用しながら、安全で使いやすい認証システムの構築を目指しましょう。

ユーザー登録・ログイン機能

ユーザー認証システムの最初のステップは、ユーザーがアカウントを作成できる登録機能です。この機能を実装するには、まずユーザーデータを保存するためのモデルを定義する必要があります。

from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin

db = SQLAlchemy()

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(128), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    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)

ユーザー登録フォームを作成するには、Flask-WTFを使用してフォームクラスを定義します。

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, EqualTo, Length

class RegistrationForm(FlaskForm):
    username = StringField('ユーザー名', 
        validators=[DataRequired(), Length(min=3, max=80)])
    email = StringField('メールアドレス', 
        validators=[DataRequired(), Email()])
    password = PasswordField('パスワード', 
        validators=[DataRequired(), Length(min=6)])
    confirm_password = PasswordField('パスワード確認', 
        validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('登録')

登録処理を行うルートは以下のように実装します。

from flask import render_template, redirect, url_for, flash
from flask_login import login_user

@app.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        # ユーザー名とメールアドレスの重複チェック
        existing_user = User.query.filter(
            (User.username == form.username.data) | 
            (User.email == form.email.data)
        ).first()

        if existing_user:
            flash('このユーザー名またはメールアドレスは既に使用されています', 'error')
            return render_template('register.html', form=form)

        # 新しいユーザーを作成
        user = User(
            username=form.username.data,
            email=form.email.data
        )
        user.set_password(form.password.data)

        db.session.add(user)
        db.session.commit()

        flash('アカウントが正常に作成されました。ログインしてください。', 'success')
        return redirect(url_for('login'))

    return render_template('register.html', form=form)

ログイン機能も同様に、フォームとルートを実装します。

class LoginForm(FlaskForm):
    username = StringField('ユーザー名', validators=[DataRequired()])
    password = PasswordField('パスワード', validators=[DataRequired()])
    remember_me = BooleanField('ログイン状態を保持する')
    submit = SubmitField('ログイン')

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()

        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember_me.data)
            flash('ログインに成功しました', 'success')
            next_page = request.args.get('next')
            return redirect(next_page) if next_page else redirect(url_for('dashboard'))
        else:
            flash('ユーザー名またはパスワードが正しくありません', 'error')

    return render_template('login.html', form=form)

パスワードハッシュ化

パスワードの安全な管理は認証システムにおいて最も重要な要素の一つです。平文のパスワードをデータベースに保存することは絶対に避けなければなりません。代わりに、パスワードをハッシュ化して保存する必要があります。

ハッシュ化とは、元のデータから固定長の文字列を生成する数学的な処理です。この処理にはいくつかの重要な特性があります。まず、ハッシュ値から元のパスワードを推測することが事実上不可能であること。第二に、同じパスワードからは常に同じハッシュ値が生成されること。第三に、わずかに異なるパスワードでも全く異なるハッシュ値が生成されることです。

Flaskでは、Werkzeugライブラリが提供する安全なパスワードハッシュ化機能を使用できます。このライブラリはデフォルトでPBKDF2アルゴリズムを使用し、ソルトと呼ばれるランダムな値を追加してレインボーテーブル攻撃を防ぎます。

from werkzeug.security import generate_password_hash, check_password_hash

# パスワードのハッシュ化例
password = 'my_secure_password'
hashed_password = generate_password_hash(password)
print(hashed_password)
# 出力例: 'pbkdf2:sha256:260000$X4R3A1L8S5T6R9I0N$...'

# パスワードの検証
is_correct = check_password_hash(hashed_password, password)
print(is_correct)  # True

ユーザーモデルには、パスワードを安全に設定し検証するためのメソッドを実装します。

class User(UserMixin, db.Model):
    # ... 既存のフィールド ...

    def set_password(self, password):
        """パスワードをハッシュ化して設定"""
        if len(password) < 6:
            raise ValueError('パスワードは6文字以上である必要があります')
        self.password_hash = generate_password_hash(
            password, 
            method='pbkdf2:sha256', 
            salt_length=16
        )

    def check_password(self, password):
        """パスワードが正しいか検証"""
        return check_password_hash(self.password_hash, password)

パスワードの強度をさらに高めるために、追加のバリデーションを実装することもできます。

import re

def validate_password_strength(password):
    """パスワードの強度を検証"""
    if len(password) < 8:
        return False, 'パスワードは8文字以上である必要があります'

    if not re.search(r'[A-Z]', password):
        return False, 'パスワードには少なくとも1つの大文字を含めてください'

    if not re.search(r'[a-z]', password):
        return False, 'パスワードには少なくとも1つの小文字を含めてください'

    if not re.search(r'\d', password):
        return False, 'パスワードには少なくとも1つの数字を含めてください'

    if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
        return False, 'パスワードには少なくとも1つの特殊文字を含めてください'

    return True, 'パスワードの強度は十分です'

セッション管理

ユーザーがログインした後、そのログイン状態を維持するためにはセッション管理が必要です。Flask-Loginはこのプロセスを簡素化する優れた拡張機能です。

まず、Flask-Loginを初期化します。

from flask_login import LoginManager

login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
login_manager.login_message = 'このページにアクセスするにはログインが必要です'
login_manager.login_message_category = 'info'

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

保護されたルートには、login_requiredデコレータを追加します。

from flask_login import login_required, current_user

@app.route('/dashboard')
@login_required
def dashboard():
    return render_template('dashboard.html', user=current_user)

@app.route('/profile')
@login_required
def profile():
    return render_template('profile.html', user=current_user)

ログアウト機能も簡単に実装できます。

from flask_login import logout_user

@app.route('/logout')
@login_required
def logout():
    logout_user()
    flash('ログアウトしました', 'info')
    return redirect(url_for('index'))

セッションの設定をカスタマイズして、セキュリティを強化することも重要です。

app.config.update(
    SECRET_KEY='your-secret-key-here',
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SECURE=True,  # HTTPSでのみ動作
    SESSION_COOKIE_SAMESITE='Lax',
    PERMANENT_SESSION_LIFETIME=timedelta(days=7)
)

ユーザーのロールや権限に基づいたアクセス制御を実装する場合は、カスタムデコレータを作成できます。

from functools import wraps
from flask_login import current_user
from flask import abort

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)
        return f(*args, **kwargs)
    return decorated_function

@app.route('/admin')
@admin_required
def admin_panel():
    return render_template('admin.html')

まとめ

Flaskを使用したユーザー認証システムの構築は、適切なツールと知識があれば比較的 straightforward なプロセスです。この記事で説明したように、ユーザー登録とログイン機能の実装、パスワードの安全なハッシュ化、そしてセッション管理の確立という3つの主要なコンポーネントを組み合わせることで、堅牢な認証システムを作成できます。

パスワードの取り扱いには特に注意を払う必要があります。Werkzeugのハッシュ化機能を使用することで、セキュリティ基準を満たした安全なパスワード管理を実現できます。また、Flask-Loginを活用すると、セッション管理の複雑な部分を抽象化し、開発者はアプリケーションの本質的な機能に集中することができます。

実際のプロダクション環境では、さらにセキュリティを強化するために、二要素認証の導入、ログイン試行回数の制限、定期的なパスワード変更の推奨などの機能を追加することを検討すべきです。

演習問題

初級問題(3問)

初級1: ユーザーモデルの作成

基本的なユーザーモデルを作成してください。以下のフィールドを含めること。

  • id(主キー)
  • username(ユニーク、必須)
  • email(ユニーク、必須)
  • password_hash(必須)
  • created_at(デフォルトで現在時刻)
# ここにコードを記述してください
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

db = SQLAlchemy()

class User(db.Model):
    # コードを完成させてください

初級2: パスワードハッシュ化の実装

Userモデルにパスワードを安全に設定・検証するメソッドを追加してください。

  • set_passwordメソッド(パスワードをハッシュ化して保存)
  • check_passwordメソッド(パスワードが正しいか検証)
from werkzeug.security import generate_password_hash, check_password_hash

class User(db.Model):
    # ... 既存のコード ...

    # ここにメソッドを追加してください

初級3: ログインフォームの作成

ユーザーログイン用のフォームクラスを作成してください。

  • ユーザー名(必須)
  • パスワード(必須)
  • 「ログイン状態を保持する」チェックボックス
  • 送信ボタン
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired

# ここにLoginFormクラスを作成してください

中級問題(6問)

中級1: ユーザー登録機能の実装

ユーザー登録用のルートとフォームを作成してください。

  • ユーザー名、メールアドレス、パスワード、パスワード確認のフィールド
  • パスワード確認のバリデーション
  • 重複ユーザーのチェック
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user

class RegistrationForm(FlaskForm):
    # フォームクラスを完成させてください

@app.route('/register', methods=['GET', 'POST'])
def register():
    # 登録処理を実装してください

中級2: ログイン機能の実装

ログイン処理を完成させてください。正しい認証情報の場合にダッシュボードにリダイレクトすること。

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # ログイン処理を完成させてください
        pass
    return render_template('login.html', form=form)

中級3: セッション管理の設定

Flask-Loginを設定し、ユーザーローダーと保護されたルートを作成してください。

from flask_login import LoginManager, login_required, current_user

# Flask-Loginの設定を完成させてください
login_manager = LoginManager()

# ユーザーローダー関数を実装してください

# 保護されたダッシュボードルートを作成してください

中級4: パスワード強度の検証

パスワードの強度を検証する関数を作成してください。

  • 8文字以上
  • 大文字・小文字・数字・特殊文字をそれぞれ1つ以上含む
import re

def validate_password_strength(password):
    # パスワード強度検証関数を完成させてください
    pass

中級5: ログアウト機能の実装

ログアウト機能と、ログアウト後のリダイレクト処理を実装してください。

from flask_login import logout_user

@app.route('/logout')
def logout():
    # ログアウト処理を実装してください
    pass

中級6: フラッシュメッセージの実装

適切なフラッシュメッセージを登録・ログイン・ログアウト機能に追加してください。

# 各ルート関数に適切なフラッシュメッセージを追加
flash('アカウントが正常に作成されました', 'success')
flash('ログインに失敗しました', 'error')

上級問題(3問)

上級1: プロフィール編集機能

ユーザーが自分のプロフィール(ユーザー名、メールアドレス)を編集できる機能を実装してください。現在のパスワード確認を必須とすること。

class ProfileForm(FlaskForm):
    # プロフィール編集フォームを作成してください
    pass

@app.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
    # プロフィール編集処理を実装してください
    pass

上級2: パスワードリセット機能

パスワードリセット機能の基礎を実装してください。

  • メールアドレス入力フォーム
  • 仮のリセットトークン生成
  • パスワード再設定フォーム
class RequestResetForm(FlaskForm):
    email = StringField('メールアドレス', validators=[DataRequired(), Email()])
    submit = SubmitField('パスワードリセットをリクエスト')

class ResetPasswordForm(FlaskForm):
    password = PasswordField('新しいパスワード', validators=[DataRequired()])
    confirm_password = PasswordField('パスワード確認', 
                                   validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('パスワードをリセット')

# ルート関数を実装してください

上級3: 管理者権限の実装

管理者権限を持つユーザーを作成し、管理者専用ページを実装してください。

class User(db.Model):
    # ... 既存のフィールド ...
    is_admin = db.Column(db.Boolean, default=False)

def admin_required(f):
    # 管理者専用デコレータを作成してください
    pass

@app.route('/admin')
@admin_required
def admin_panel():
    # 管理者ページを実装してください
    pass

解答例

初級問題解答例(3問)

初級1 解答例

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(128), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

初級2 解答例

class User(db.Model):
    # ... 既存のフィールド ...

    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)

初級3 解答例

class LoginForm(FlaskForm):
    username = StringField('ユーザー名', validators=[DataRequired()])
    password = PasswordField('パスワード', validators=[DataRequired()])
    remember_me = BooleanField('ログイン状態を保持する')
    submit = SubmitField('ログイン')

中級問題解答例(6問)

中級1 解答例

class RegistrationForm(FlaskForm):
    username = StringField('ユーザー名', validators=[DataRequired(), Length(min=3, max=80)])
    email = StringField('メールアドレス', validators=[DataRequired(), Email()])
    password = PasswordField('パスワード', validators=[DataRequired(), Length(min=6)])
    confirm_password = PasswordField('パスワード確認', 
                                   validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('登録')

@app.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        existing_user = User.query.filter(
            (User.username == form.username.data) | 
            (User.email == form.email.data)
        ).first()
        if existing_user:
            flash('このユーザー名またはメールアドレスは既に使用されています', 'error')
            return render_template('register.html', form=form)

        user = User(username=form.username.data, email=form.email.data)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()

        flash('アカウントが正常に作成されました', 'success')
        return redirect(url_for('login'))

    return render_template('register.html', form=form)

中級2 解答例

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember_me.data)
            flash('ログインに成功しました', 'success')
            next_page = request.args.get('next')
            return redirect(next_page) if next_page else redirect(url_for('dashboard'))
        else:
            flash('ユーザー名またはパスワードが正しくありません', 'error')
    return render_template('login.html', form=form)

中級3 解答例

login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

@app.route('/dashboard')
@login_required
def dashboard():
    return render_template('dashboard.html', user=current_user)

中級4 解答例

def validate_password_strength(password):
    if len(password) < 8:
        return False, 'パスワードは8文字以上である必要があります'
    if not re.search(r'[A-Z]', password):
        return False, 'パスワードには少なくとも1つの大文字を含めてください'
    if not re.search(r'[a-z]', password):
        return False, 'パスワードには少なくとも1つの小文字を含めてください'
    if not re.search(r'\d', password):
        return False, 'パスワードには少なくとも1つの数字を含めてください'
    if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
        return False, 'パスワードには少なくとも1つの特殊文字を含めてください'
    return True, 'パスワードの強度は十分です'

中級5 解答例

@app.route('/logout')
@login_required
def logout():
    logout_user()
    flash('ログアウトしました', 'info')
    return redirect(url_for('index'))

中級6 解答例

#(各ルートに適切なフラッシュメッセージを追加)

上級問題解答例(3問)

上級1 解答例

class ProfileForm(FlaskForm):
    username = StringField('ユーザー名', validators=[DataRequired(), Length(min=3, max=80)])
    email = StringField('メールアドレス', validators=[DataRequired(), Email()])
    current_password = PasswordField('現在のパスワード', validators=[DataRequired()])
    submit = SubmitField('プロフィールを更新')

@app.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
    form = ProfileForm()
    if form.validate_on_submit():
        if current_user.check_password(form.current_password.data):
            current_user.username = form.username.data
            current_user.email = form.email.data
            db.session.commit()
            flash('プロフィールが更新されました', 'success')
            return redirect(url_for('dashboard'))
        else:
            flash('現在のパスワードが正しくありません', 'error')
    elif request.method == 'GET':
        form.username.data = current_user.username
        form.email.data = current_user.email
    return render_template('profile.html', form=form)

上級2 解答例(基本的な実装)

@app.route('/reset_password_request', methods=['GET', 'POST'])
def reset_password_request():
    form = RequestResetForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user:
            # 実際の実装ではここでトークンを生成してメールを送信
            flash('パスワードリセットの手順をメールで送信しました', 'info')
        else:
            flash('このメールアドレスは登録されていません', 'error')
        return redirect(url_for('login'))
    return render_template('reset_password_request.html', form=form)

上級3 解答例

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)
        return f(*args, **kwargs)
    return decorated_function

@app.route('/admin')
@admin_required
def admin_panel():
    users = User.query.all()
    return render_template('admin.html', users=users)