【Flask + WebAuthn】スマホ対応の家計簿Webアプリを作ってみた【パスキー認証】

こんにちは、七宮さん(@shichinomiya_s)です。

今回の記事では、FlaskベースでモバイルファーストなWeb会計アプリケーションを構築したお話を書こうと思います。American Expressのクレジットカード明細を手動でインポートして管理できる仕組みも実装しました。

最近、家計簿アプリは色々ありますが、「自分でデータを管理したい」「スマホでサクッと入力したい」「セキュリティもしっかりしたい」という要望を満たすために、完全自作してみました。

はじめに:なぜ自作したのか

家計簿アプリは市販のものも多いですが、以下の理由で自作することにしました:

  • データの完全な所有権:クラウドサービスではなく、自分のサーバー(NAS等)で完全に管理したい
  • カスタマイズ性:カテゴリや機能を自由に追加・変更したい
  • スマホ対応:外出先でもサクッと入力できるモバイルファーストなUI
  • セキュリティ:最新のWebAuthn(パスキー)認証を実装してみたい
  • AMEXとの連携:クレジットカード明細を効率的に取り込みたい

既存のツールでは痒いところに手が届かないことが多かったので、思い切って自作することにしました。

アプリケーションの機能概要

このWeb会計アプリには以下の機能を実装しています:

主要機能

  • 支出記録の入力・表示(スマートフォン最適化)
  • カテゴリ別支出集計(食費、日用品、医療費など27カテゴリ)
  • 月次レポート
  • WebAuthn(パスキー)認証
  • 管理者機能(アカウント管理、カテゴリ管理)
  • ログイン試行制限・IP BANシステム
  • レシート画像アップロード
  • 月次精算機能
  • 公共料金管理ダッシュボード
  • AMEXデータ手動インポート(CSVファイル経由)

画面構成

主な画面は以下の通りです:

  • ログイン画面:WebAuthn(パスキー)による生体認証
  • 支出記録入力画面:日付、店舗、カテゴリ、金額等を入力
  • 記録一覧画面:月別の支出一覧とカテゴリ別集計
  • 管理画面:カテゴリ管理、アカウント管理、BAN管理

ログイン画面

WebAuthnによるパスキー認証

支出記録入力画面

モバイルファーストなUI

記録一覧画面

カテゴリ別の支出集計

カテゴリ管理画面

カテゴリのカスタマイズ

技術スタック

このアプリケーションは以下の技術で構築しています:

Backend

  • Flask:Pythonの軽量Webフレームワーク
  • Pandas:データ処理とCSV操作
  • WebAuthn:パスキー認証(webauthn>=2.0.0
  • Cryptography:暗号化処理(cryptography>=41.0.0

Frontend

  • Bootstrap 5:レスポンシブUIフレームワーク
  • Font Awesome:アイコン
  • Apple Design System:Apple風のカスタムCSS
  • Progressive Web App (PWA):スマホのホーム画面に追加可能

データ管理

  • CSV:支出データの保存(年月別)
  • JSON:設定データ、ユーザー情報、カテゴリ情報

セキュリティ

  • HTTPS:SSL/TLS通信
  • WebAuthn:FIDO2準拠のパスキー認証
  • セッション管理:Secure Cookie、HTTPOnly、SameSite属性
  • IP BAN:ログイン試行回数制限

システム構成図

システム全体の構成図。クライアント、サーバー、データ層が明確に分離されています。

プロジェクト構成

プロジェクトのディレクトリ構成は以下のようになっています:

web_accounting/
├── app.py                     # メインアプリケーション
├── auth.py                    # 認証モジュール(WebAuthn実装)
├── requirements.txt           # Python依存関係
├── cert.pem / key.pem        # SSL証明書(HTTPS用)
├── templates/                # HTMLテンプレート
│   ├── base.html             # ベーステンプレート
│   ├── index.html            # 支出記録入力画面
│   ├── view.html             # 記録一覧画面
│   ├── login.html            # ログイン画面
│   ├── register.html         # ユーザー登録画面
│   ├── manage_categories.html # カテゴリ管理画面
│   ├── manage_accounts.html   # アカウント管理画面
│   └── manage_bans.html       # BAN管理画面
├── static/                   # 静的ファイル
│   ├── apple-design.css      # Apple風カスタムCSS
│   ├── icons/                # PWA用アイコン
│   ├── manifest.json         # PWAマニフェスト
│   └── sw.js                 # Service Worker
└── data/                     # データディレクトリ
    ├── categories.json       # カテゴリ設定
    ├── users.json            # ユーザー情報
    ├── login_attempts.json   # ログイン試行記録
    ├── csv/                  # 会計データ(年月別CSV)
    │   └── 2025/
    │       ├── 202501.csv
    │       └── 202502.csv
    └── receipts/             # レシート画像保存先

セットアップ手順

1. 依存関係のインストール

まず、必要なPythonパッケージをインストールします:

cd web_accounting
pip install -r requirements.txt

requirements.txt の内容はこんな感じです:

flask
pandas
python-dateutil
webauthn>=2.0.0
cryptography>=41.0.0

シンプルですね。

2. データディレクトリの準備

初回起動時に自動的に作成されますが、手動で作成する場合は以下のコマンドで:

mkdir -p data/csv data/receipts

3. SSL証明書の生成(HTTPS用)

WebAuthn(パスキー)認証はHTTPS環境が必須です。自己署名証明書を生成します:

python generate_cert.py

これで cert.pemkey.pem が生成されます。

本番環境では、Let’s Encryptなどで正式な証明書を取得することをおすすめします。

4. アプリケーションの起動

python app.py

デフォルトではポート5000でHTTPSサーバーが起動します。

 * Running on https://0.0.0.0:5000

5. ブラウザでアクセス

スマートフォンやPCのブラウザから以下のURLでアクセス:

https://[サーバーのIPアドレス]:5000

自己署名証明書の場合、ブラウザで警告が出ますが、「詳細設定」→「このサイトにアクセスする」で進めます。

6. 初回ユーザー登録

初回アクセス時に、ユーザー登録画面が表示されます。WebAuthnに対応したデバイス(指紋認証、顔認証、セキュリティキー等)を使用して登録します。

パスキーを掌握されると困るので、私の環境では専用ページを作成し、Cloudflare Access で上位レイヤーで防御しています。

ユーザー登録画面1

ユーザー名入力

ユーザー登録画面2

パスキー登録

ユーザー登録画面3

登録完了

主要機能の実装解説

ここからは、主要な機能の実装について詳しく見ていきます。

WebAuthn(パスキー)認証

このアプリの目玉機能の一つが、WebAuthn(パスキー)認証です。従来のパスワード認証ではなく、FIDO2準拠の生体認証やセキュリティキーを使った認証を実装しました。

WebAuthnとは?

WebAuthnは、Webサイトやアプリで生体認証(指紋、顔認証)やセキュリティキーを使って安全にログインできる仕組みです。パスワードを覚える必要がなく、フィッシング攻撃にも強いという特徴があります。

最近では、Apple、Google、Microsoftが推進している「パスキー」という名前でも知られています。

認証フロー

WebAuthnの登録フローとログインフロー。パスワード不要で安全性が高いのが特徴です。

実装のポイント

auth.py に認証ロジックを実装しています。主なポイントは以下の通りです:

from webauthn import (
    generate_registration_options,
    verify_registration_response,
    generate_authentication_options,
    verify_authentication_response,
)

# ユーザー登録時のチャレンジ生成
def start_registration(username, user_id):
    """WebAuthn登録開始"""
    options = generate_registration_options(
        rp_id=get_rp_id(),
        rp_name="家計簿システム",
        user_id=user_id.encode('utf-8'),
        user_name=username,
        authenticator_selection=AuthenticatorSelectionCriteria(
            user_verification=UserVerificationRequirement.REQUIRED,
            resident_key=ResidentKeyRequirement.REQUIRED,
        ),
    )
    return options

# ログイン時のチャレンジ生成
def start_authentication(username):
    """WebAuthn認証開始"""
    user = get_user_by_username(username)
    if not user:
        return None

    options = generate_authentication_options(
        rp_id=get_rp_id(),
        allow_credentials=[
            PublicKeyCredentialDescriptor(id=base64.b64decode(cred['id']))
            for cred in user['credentials']
        ],
    )
    return options

WebAuthnライブラリの webauthn>=2.0.0 を使うことで、比較的簡単に実装できました。

認証の流れ

認証の流れはこんな感じです:

  1. 登録フロー
    • ユーザーがユーザー名を入力
    • サーバーがチャレンジ(ランダムな文字列)を生成
    • ブラウザがWebAuthn APIを呼び出し、生体認証を実行
    • 公開鍵がサーバーに保存される
  2. ログインフロー
    • ユーザーがユーザー名を入力
    • サーバーがチャレンジを生成
    • ブラウザがWebAuthn APIを呼び出し、生体認証を実行
    • サーバーが署名を検証してログイン成功

パスワードを一切使わないので、とても安全ですね。

Apple風UIデザイン

UIデザインには、Apple Design Systemを参考にしたカスタムCSSを作成しました。

デザインコンセプト

Appleのデザインといえば、シンプルで洗練された印象ですよね。以下の要素を取り入れました:

  • SF Pro Displayフォント風のシステムフォント
  • グラスモーフィズム効果(半透明のカード)
  • 鮮やかなアクセントカラー(Apple Blue、Apple Orange等)
  • 滑らかなアニメーション(cubic-bezierイージング)
  • 適度な余白と角丸

カスタムCSSの例

static/apple-design.css には、Apple風のスタイルを定義しています:

:root {
    --apple-blue: #007AFF;
    --apple-gray: #F2F2F7;
    --apple-border: #E5E5EA;
    --apple-text: #1C1C1E;
    --apple-red: #FF3B30;
    --apple-green: #34C759;
    --apple-orange: #FF9500;
}

/* グラスモーフィズム効果 */
.glass-card {
    background: rgba(255, 255, 255, 0.8);
    backdrop-filter: saturate(180%) blur(20px);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    border: 0.5px solid rgba(255, 255, 255, 0.3);
}

/* Apple風のホバー効果 */
.apple-hover {
    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}

.apple-hover:hover {
    transform: scale(1.02);
}

.apple-hover:active {
    transform: scale(0.98);
}

backdrop-filter を使った半透明のグラスモーフィズム効果は、最近のAppleのデザインでよく見られますね。スマホで見ると特に美しいです。

レスポンシブデザイン

モバイルファーストで設計しているので、スマホでの操作性を重視しています:

  • タッチ操作に最適化されたボタンサイズ
  • スワイプジェスチャーに対応
  • フローティングアクションボタン(FAB)で素早く入力
  • 大きめのフォントサイズで視認性向上

Bootstrap 5のグリッドシステムと組み合わせることで、画面サイズに応じて最適なレイアウトを提供しています。

支出記録機能

家計簿アプリのコア機能である支出記録について見ていきます。

入力フォーム

templates/index.html には、支出記録の入力フォームを実装しています:

<form action="{{ url_for('add_expense') }}" method="POST" enctype="multipart/form-data">
    <div class="row">
        <!-- 日付 -->
        <div class="col-md-6 mb-3">
            <label for="date" class="form-label">
                <i class="fas fa-calendar me-1"></i>日付
            </label>
            <input type="date" class="form-control" id="date" name="date" required>
        </div>

        <!-- 利用店舗 -->
        <div class="col-md-6 mb-3">
            <label for="shop" class="form-label">
                <i class="fas fa-store me-1"></i>利用店舗
            </label>
            <input type="text" class="form-control" id="shop" name="shop" required>
        </div>

        <!-- カテゴリ -->
        <div class="col-md-6 mb-3">
            <label for="category" class="form-label">
                <i class="fas fa-tags me-1"></i>カテゴリ
            </label>
            <select class="form-select" id="category" name="category" required>
                <option value="">カテゴリを選択</option>
                {% for cat in categories %}
                    <option value="{{ cat }}">{{ cat }}</option>
                {% endfor %}
            </select>
        </div>

        <!-- 支払方法 -->
        <div class="col-md-6 mb-3">
            <label for="payment_method" class="form-label">
                <i class="fas fa-credit-card me-1"></i>支払方法
            </label>
            <select class="form-select" id="payment_method" name="payment_method" required>
                <!-- 省略 -->
            </select>
        </div>

        <!-- 金額 -->
        <div class="col-md-6 mb-3">
            <label for="amount" class="form-label">
                <i class="fas fa-yen-sign me-1"></i>決済金額
            </label>
            <input type="number" class="form-control" id="amount" name="amount" required>
        </div>
    </div>
</form>

Font Awesomeのアイコンを各入力項目に付けることで、視覚的に分かりやすくしています。

データ保存処理

app.pyadd_expense() 関数で、入力されたデータをCSVファイルに保存します:

@app.route('/add', methods=['POST'])
@login_required
def add_expense():
    """支出記録を追加"""
    # フォームデータを取得
    date = request.form.get('date')
    shop = request.form.get('shop')
    category = request.form.get('category')
    payment_method = request.form.get('payment_method')
    amount = request.form.get('amount')

    # 年月を抽出
    year_month = datetime.strptime(date, '%Y-%m-%d').strftime('%Y%m')

    # CSVファイルパスを生成
    csv_path = os.path.join(CSV_BASE_DIR, f"{year_month[:4]}", f"{year_month}.csv")

    # 新しいレコードを作成
    new_record = {
        '日付': date,
        '店舗': shop,
        'カテゴリ': category,
        '支払方法': payment_method,
        '金額': amount,
    }

    # CSVに追記
    # (詳細なコードは省略)

    flash('記録を追加しました', 'success')
    return redirect(url_for('index'))

データはCSVファイルとして年月別に保存されるので、後からExcelなどで分析することもできますね。

カテゴリ管理

カテゴリは data/categories.json で管理しています:

{
  "categories": [
    "食費 (家庭)",
    "食費 (外食)",
    "日用品",
    "消耗品",
    "医療費",
    "交通費",
    "その他"
  ],
  "payment_method": [
    "クレジットカード",
    "現金",
    "電子マネー"
  ]
}

管理画面からカテゴリの追加・削除・並び替えができるようになっています。

カテゴリ管理画面1

カテゴリ一覧

カテゴリ管理画面2

ドラッグ&ドロップで並び替え

ログイン制限・BAN機能

セキュリティ対策として、ログイン試行回数制限IP BANシステムを実装しました。

仕組み

  • 最大試行回数:5回まで
  • BAN期間:24時間
  • 試行回数リセット:30分間ログイン試行がない場合

auth.py に実装されています:

# ログイン制限設定
MAX_LOGIN_ATTEMPTS = 5
BAN_DURATION_HOURS = 24
ATTEMPT_RESET_MINUTES = 30

def is_ip_banned(ip_address):
    """IPアドレスがBANされているか確認"""
    ban_data = load_ban_list()
    current_time = datetime.now()

    for ban_entry in ban_data['banned_ips']:
        if ban_entry['ip'] == ip_address:
            ban_time = datetime.fromisoformat(ban_entry['banned_at'])
            ban_until = ban_time + timedelta(hours=BAN_DURATION_HOURS)

            if current_time < ban_until:
                return True, ban_until

    return False, None

def record_login_attempt(ip_address, success=False):
    """ログイン試行を記録"""
    attempts_data = load_login_attempts()

    if success:
        # 成功したら試行記録をクリア
        if ip_address in attempts_data['attempts']:
            del attempts_data['attempts'][ip_address]
    else:
        # 失敗したら試行回数を増やす
        if ip_address not in attempts_data['attempts']:
            attempts_data['attempts'][ip_address] = []

        attempts_data['attempts'][ip_address].append({
            'timestamp': datetime.now().isoformat()
        })

        # 最大試行回数を超えたらBAN
        if len(attempts_data['attempts'][ip_address]) >= MAX_LOGIN_ATTEMPTS:
            ban_ip(ip_address)

    save_login_attempts(attempts_data)

これにより、ブルートフォース攻撃を防ぐことができます。管理画面から、BANされたIPアドレスの確認や解除もできるようにしています。

AMEXデータの手動インポート

American Expressのクレジットカード明細を手動でインポートする仕組みも実装しました。

AMEXデータの手動インポートフロー。5つのステップで簡単にデータを取り込めます。

フロー

  1. AMEXサイトから明細をダウンロード
    • American Expressの公式サイトにログイン
    • 「利用明細」→「ダウンロード」
    • CSV形式でダウンロード(ファイル名: activity.csv
  2. データ変換スクリプトを実行
    • ダウンロードしたCSVを AMEX/raw_data/ フォルダに配置
    • 変換スクリプトを実行:
cd AMEX
python convert_activity.py
  1. 変換されたデータを確認
    • AMEX/data/ フォルダに年月別のCSVファイルが生成されます
    • ファイル名例:activity_converted_202501.csv

convert_activity.py の処理内容

変換スクリプトは以下の処理を行います:

import pandas as pd
import os

def convert_activity_data():
    """AMEXデータの変換処理"""
    # 元データを読み込み(Shift-JISエンコーディング)
    df = pd.read_csv('raw_data/activity.csv', encoding='shift_jis')

    # 必要な列のみ抽出
    required_columns = ['ご利用日', 'ご利用内容', 'カード会員様名', '金額']
    df_filtered = df[required_columns].copy()

    # 金額を数値に変換(カンマ削除)
    df_filtered['金額'] = df_filtered['金額'].str.replace(',', '').astype(float)

    # マイナス金額を除外(返金データ等)
    df_filtered = df_filtered[df_filtered['金額'] > 0]

    # 日付でフィルタリング(指定日以降のデータのみ)
    df_filtered['ご利用日'] = pd.to_datetime(df_filtered['ご利用日'])
    df_filtered = df_filtered[df_filtered['ご利用日'] >= '2025-07-01']

    # 年月ごとにグループ化して保存
    df_filtered['年月'] = df_filtered['ご利用日'].dt.strftime('%Y%m')
    grouped = df_filtered.groupby('年月')

    for year_month, group_df in grouped:
        output_path = f"data/activity_converted_{year_month}.csv"
        group_df.to_csv(output_path, index=False, encoding='shift_jis')
        print(f"保存完了: {output_path}")

Web会計アプリへの取り込み

変換されたCSVファイルは、以下の方法でWeb会計アプリに取り込めます:

方法1:手動でデータをマージ

  • 変換後のCSVファイルを web_accounting/data/csv/[年]/[年月].csv にコピー
  • 既存のデータがある場合は、Excelなどで開いて手動でマージ

方法2:アプリ内で個別に入力

  • 変換後のCSVファイルを参照しながら、Web会計アプリの入力画面から手動で入力
  • カテゴリを選択しながら入力できるので、より細かい分類が可能

私は通常、方法2を使っています。AMEXの明細をざっと確認しながら、適切なカテゴリを振り分けられるので便利ですね。

セキュリティ対策

個人的な金融データを扱うアプリなので、セキュリティには特に気を使いました。

実装したセキュリティ対策

対策項目 実装内容
HTTPS通信 SSL/TLS証明書によるHTTPS通信を必須化
WebAuthn認証 FIDO2準拠のパスキー認証、パスワード不要
セッション管理 Secure Cookie、HTTPOnly、SameSite属性を設定
IP BAN 5回のログイン失敗で24時間BAN
CSRF対策 FlaskのセッションとSameSite属性で対策
ファイルアップロード制限 画像ファイルのみ許可、サイズ制限16MB
管理者認証 管理画面は別途パスワード認証を実装
データの暗号化 セッションキーは環境変数で管理

セキュリティ設定のコード例

app.py でのセキュリティ設定:

# セッション設定
app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'default_secret_key')
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30)
app.config['SESSION_COOKIE_SECURE'] = True      # HTTPS必須
app.config['SESSION_COOKIE_HTTPONLY'] = True    # JS無効化
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'   # CSRF対策

# ファイルアップロード制限
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

本番環境では、FLASK_SECRET_KEY を環境変数で設定し、推測不可能なランダム文字列を使用することをおすすめします。

管理者パスワードの設定

管理画面にアクセスするための管理者パスワードは、SHA256ハッシュで保存しています:

# デフォルトパスワードのハッシュ
DEFAULT_PASSWORD_HASH = "7c4b006def035d37fd5d9f8a67a2fb73626ae2c3a502573bc026d80aebda1930"

def hash_password(password):
    """パスワードをSHA256でハッシュ化"""
    return hashlib.sha256(password.encode('utf-8')).hexdigest()

本番環境では、環境変数 ADMIN_PASSWORD_HASH で独自のハッシュ値を設定してください。

実際に使ってみた感想

このアプリを数ヶ月間実際に運用してみた感想をまとめます。

良かった点

  • スマホでサクッと入力できる:外出先でもその場で入力できるのが便利
  • パスキー認証が快適:指紋認証でログインできるので、パスワード入力が不要
  • カスタマイズ性が高い:カテゴリや機能を自由に追加できる
  • データの所有権:NASに保存しているので、完全に自分で管理できる
  • AMEXとの連携:クレジットカード明細を効率的に取り込める
  • Apple風UIが綺麗:見た目が良いとモチベーションが上がる

改善したい点

  • CSVインポート機能の強化:Web画面から直接CSVアップロードできると便利
  • グラフ機能の追加:カテゴリ別の支出推移をグラフで可視化したい
  • 予算管理機能:月ごとの予算を設定して、超過を警告する機能
  • 複数ユーザーでの共有:家族で支出データを共有する仕組み
  • 自動バックアップ:定期的にデータをバックアップする仕組み

といった感じで、まだまだ改善の余地はありますね。今後のアップデートで順次追加していこうと思います。

パフォーマンス

Synology NAS(DS723+)上でDockerコンテナとして動かしていますが、レスポンスは非常に良好です。Flaskは軽量なので、NASのような非力なハードウェアでも快適に動作します。

データ量が増えても、CSVファイルを年月別に分割しているので、パフォーマンスの劣化は今のところ感じていません。

さいごに

今回の記事では、FlaskベースのモバイルファーストなWeb会計アプリケーションを構築したお話を書きました。

WebAuthn(パスキー)認証、Apple風UI、AMEXデータのインポートなど、実用的な機能を詰め込んだアプリになったかと思います。

自分でデータを管理したい、カスタマイズ性の高い家計簿アプリが欲しい、という方にはおすすめです。コード全体はGitHubで公開する予定はありませんが、この記事が参考になれば幸いです。

今後の展開

今後は以下の機能を追加していく予定です:

  • Web画面からCSVインポート機能
  • Chart.jsを使ったグラフ可視化
  • 予算管理・警告機能
  • 月次レポートのPDFエクスポート
  • 複数ユーザー対応(家族アカウント)
  • 定期支出の自動入力機能
  • データの自動バックアップ

もし「こんな機能があったらいいな」というアイデアがあれば、コメントで教えていただけると嬉しいです。

いかがだったでしょうか。この記事が、Web会計アプリを自作してみたい方の参考になれば幸いです。


関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です