Flaskでのユーザー登録・ログイン機能の実装

実装前に理解しておくべき重要ポイント

CSRFトークンの取得方法

クライアントサイドでCookieからCSRFトークンを取得する関数です:

function getCsrfToken(tokenName) {
    var pattern = new RegExp("\\b" + tokenName + "=([^;]*)");
    var match = document.cookie.match(pattern);
    return match ? match[1] : null;
}

フォーム送信のカスタマイズ

Ajaxを使用してJSON形式でデータを送信するためフォームのデフォルト動作を無効化します:

$(".registration-form").on("submit", function(event) {
    event.preventDefault();
    // カスタム送信ロジックをここに記述
});

AjaxによるJSONデータ送信の実装

var payload = {
    phoneNumber: phoneNum,
    verifyCode: code,
    userPassword: pwd
};

// オブジェクトをJSON文字列に変換
var jsonPayload = JSON.stringify(payload);

$.ajax({
    url: "/api/v1_0/users",
    method: "POST",
    data: jsonPayload,
    contentType: "application/json",
    dataType: "json",
    headers: {
        "X-CSRFToken": getCsrfToken("csrf_token")
    },
    complete: function(response) {
        if (response.status === 200) {
            window.location.href = "/";
        } else {
            showErrorMessage(response.responseJSON.errmsg);
        }
    }
});

パスワードの安全な管理

Werkzeugのセキュリティモジュールを活用したパスワード処理の詳細は、公式ドキュメントを参照してください:

from werkzeug.security import generate_password_hash, check_password_hash

パスワードの暗号化にはgenerate_password_hashを、照合にはcheck_password_hashを使用します。モデルクラスには以下のようにプロパティを定義します:

from werkzeug.security import generate_password_hash, check_password_hash

class User(db.Model):
    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    phone = db.Column(db.String(11), unique=True)
    password_hash = db.Column(db.String(255))

    @property
    def pwd(self):
        raise AttributeError("パスワードへの直接アクセスは禁止されています")

    @pwd.setter
    def pwd(self, rawPassword):
        # SHA256とランダムソルト(8文字)を使用したハッシュ化
        self.password_hash = generate_password_hash(rawPassword)

    def verify_password(self, inputPassword):
        """入力されたパスワードが正しいか検証"""
        return check_password_hash(self.password_hash, inputPassword)

クライアントIPアドレスの取得

FlaskのrequestオブジェクトからユーザーのIPアドレスを取得できます:

client_ip = request.remote_addr

トランザクションロールバック

from flask import session
db.session.rollback()

ユーザー登録APIの実装

API仕様

エンドポイント: /api/v1_0/users/
メソッド: POST
コンテンツタイプ: application/json
必須パラメータ: phoneNumber, verifyCode, userPassword
レスポンス: {"errno": 状態コード, "errmsg": メッセージ}

サーバーサイドの処理フロー

  1. パラメータの受領(電話番号、認証コード、パスワード)
  2. 入力値のバリデーション(全体チェック、電話番号フォーマット検証)
  3. RedisからSMS認証コードを取得し、有効期限と一致性を確認
  4. 認証成功後、Redisから認証コードを削除
  5. 新規ユーザーを作成してデータベースに保存
  6. セッションにユーザー情報を設定

バックエンド実装

# -*- coding: utf-8 -*-
from flask import Blueprint, request, jsonify, session
import re
import logging
from ihome import redis_client, database
from ihome.models import User
from ihome.constants import ResponseStatus as RET

api = Blueprint("user_api", __name__, url_prefix="/api/v1_0")

@api.route("/users", methods=["POST"])
def user_registration():
    # リクエストボディからJSONデータを取得
    request_data = request.get_json()
    phone = request_data.get("phoneNumber")
    auth_code = request_data.get("verifyCode")
    pwd = request_data.get("userPassword")

    # パラメータの存在確認
    if not all([phone, auth_code, pwd]):
        return jsonify({"errno": RET.PARAMERR, "errmsg": "パラメータが不完全です"})

    # 電話番号フォーマットの検証
    if not re.match(r"1[3-9]\d{9}", phone):
        return jsonify({"errno": RET.DATAERR, "errmsg": "電話番号の形式が不正です"})

    # RedisからSMS認証コードを取得
    try:
        stored_code = redis_client.get("sms_code_" + phone)
    except Exception as error:
        logging.error("Redisエラー: %s", error)
        return jsonify({"errno": RET.DBERR, "errmsg": "認証コードの取得に失敗しました"})

    # 認証コードの有効性チェック
    if stored_code is None:
        return jsonify({"errno": RET.NODATA, "errmsg": "認証コードの有効期限が切れています"})

    if auth_code != stored_code:
        return jsonify({"errno": RET.DATAERR, "errmsg": "認証コードが一致しません"})

    # 認証コードをRedisから削除
    try:
        redis_client.delete("sms_code_" + phone)
    except Exception as error:
        logging.error("Redis削除エラー: %s", error)

    # ユーザーオブジェクトの作成と保存
    new_user = User(name=phone, phone=phone)
    new_user.pwd = pwd

    try:
        database.session.add(new_user)
        database.session.commit()
    except Exception as error:
        logging.error("データベースエラー: %s", error)
        database.session.rollback()
        return jsonify({"errno": RET.DBERR, "errmsg": "ユーザー登録に失敗しました"})

    # セッション情報の設定
    session["user_id"] = new_user.id
    session["user_name"] = phone
    session["phone"] = phone

    return jsonify({"errno": RET.OK, "errmsg": "登録が完了しました"})

クライアントサイド実装

$(document).ready(function() {
    // イメージキャプチャ初期化
    initCaptcha();

    // 各フィールドのエラー表示非表示化
    $("#phoneNumber").on("focus", function() {
        $("#phone-error").hide();
    });
    $("#captchaInput").on("focus", function() {
        $("#captcha-error").hide();
    });
    $("#smsCode").on("focus", function() {
        $("#sms-error").hide();
    });
    $("#password").on("focus", function() {
        $("#password-error").hide();
        $("#confirm-error").hide();
    });
    $("#confirmPassword").on("focus", function() {
        $("#confirm-error").hide();
    });

    // 登録フォーム送信処理
    $(".registration-form").on("submit", function(event) {
        event.preventDefault();

        var phoneNum = $("#phoneNumber").val();
        var smsCode = $("#smsCode").val();
        var password = $("#password").val();
        var confirmPwd = $("#confirmPassword").val();

        // バリデーション
        if (!phoneNum) {
            $("#phone-error span").text("有効な電話番号を入力してください");
            $("#phone-error").show();
            return;
        }
        if (!smsCode) {
            $("#sms-error span").text("SMS認証コードを入力してください");
            $("#sms-error").show();
            return;
        }
        if (!password) {
            $("#password-error span").text("パスワードを入力してください");
            $("#password-error").show();
            return;
        }
        if (password !== confirmPwd) {
            $("#confirm-error span").text("パスワードが一致しません");
            $("#confirm-error").show();
            return;
        }

        // 送信データ構築
        var postData = {
            phoneNumber: phoneNum,
            verifyCode: smsCode,
            userPassword: password
        };

        var jsonData = JSON.stringify(postData);

        // Ajaxリクエスト送信
        $.ajax({
            url: "/api/v1_0/users",
            method: "POST",
            data: jsonData,
            contentType: "application/json",
            dataType: "json",
            headers: {
                "X-CSRFToken": getCsrfToken("csrf_token")
            },
            complete: function(result) {
                if (result.status === 200) {
                    window.location.href = "/";
                } else {
                    alert(result.responseJSON.errmsg);
                }
            }
        });
    });
});

ログインAPIの実装

API仕様

エンドポイント: /api/v1_0/sessions/
メソッド: POST
コンテンツタイプ: application/json
必須パラメータ: phoneNumber, userPassword
レスポンス: {"errno": 状態コード, "errmsg": メッセージ}

サーバーサイドの処理フロー

  1. パラメータの受領(電話番号、パスワード)
  2. パラメータのバリデーション
  3. クライアントIPアドレスを取得
  4. Redisからログイン失敗回数をチェック
  5. 5回以上の失敗がある場合はブロック
  6. ユーザー認証(電話番号とパスワードの照合)
  7. 認証成功時にRedisの失敗カウンタをリセット
  8. セッション情報の設定
  9. 結果を返す

バックエンド実装

@api.route("/sessions", methods=["POST"])
def user_login():
    # パラメータ取得
    request_body = request.get_json()
    phone = request_body.get("phoneNumber")
    pwd = request_body.get("userPassword")

    # パラメータ検証
    if not all([phone, pwd]):
        return jsonify(errno=RET.PARAMERR, errmsg="パラメータを入力してください")

    if not re.match(r"1[3-9]\d{9}", phone):
        return jsonify(errno=RET.PARAMERR, errmsg="電話番号の形式が不正です")

    # クライアントIP取得
    client_ip = request.remote_addr
    failure_key = "login_attempts_" + client_ip

    # ログイン失敗回数の確認
    try:
        attempt_count = redis_client.get(failure_key)
    except Exception as error:
        logging.error("Redis読み取りエラー: %s", error)
        return jsonify(errno=RET.DBERR, errmsg="システムエラーが発生しました")

    # 連続失敗が5回以上の場合はブロック
    if attempt_count and int(attempt_count) >= 5:
        return jsonify(errno=RET.REQERR, errmsg="ログイン試行回数が上限に達しました")

    # ユーザー存在確認とパスワード照合
    try:
        user_record = User.query.filter_by(phone=phone).first()
    except Exception as error:
        logging.error("データベースクエリエラー: %s", error)
        return jsonify(errno=RET.DBERR, errmsg="ユーザー情報の取得に失敗しました")

    # 認証処理
    if not user_record or not user_record.verify_password(pwd):
        # 失敗回数をインクリメント
        try:
            redis_client.incr(failure_key)
            redis_client.expire(failure_key, 600)  # 10分間有効
        except Exception as error:
            logging.error("Redis更新エラー: %s", error)

        return jsonify(errno=RET.LOGINERR, errmsg="認証情報が正しくありません")

    # 認証成功 - 失敗カウンタをリセット
    try:
        redis_client.delete(failure_key)
    except Exception as error:
        logging.error("Redis削除エラー: %s", error)

    # セッション設定
    session["user_id"] = user_record.id
    session["user_name"] = phone
    session["phone"] = phone

    return jsonify(errno=RET.OK, errmsg="ログインに成功しました")

クライアントサイド実装

$(document).ready(function() {
    // エラー表示の切り替え
    $("#phoneNumber").on("focus", function() {
        $("#phone-error").hide();
    });
    $("#password").on("focus", function() {
        $("#password-error").hide();
    });

    // ログインフォーム送信
    $(".login-form").on("submit", function(event) {
        event.preventDefault();

        var phoneNum = $("#phoneNumber").val();
        var password = $("#password").val();

        // 入力チェック
        if (!phoneNum) {
            $("#phone-error span").text("有効な電話番号を入力してください");
            $("#phone-error").show();
            return;
        }
        if (!password) {
            $("#password-error span").text("パスワードを入力してください");
            $("#password-error").show();
            return;
        }

        // リクエストデータ作成
        var requestPayload = {
            phoneNumber: phoneNum,
            userPassword: password
        };

        var jsonPayload = JSON.stringify(requestPayload);

        // Ajax送信
        $.ajax({
            url: "/api/v1_0/sessions",
            method: "POST",
            data: jsonPayload,
            contentType: "application/json",
            dataType: "json",
            headers: {
                "X-CSRFToken": getCsrfToken("csrf_token")
            },
            complete: function(response) {
                if (response.status === 200) {
                    window.location.href = "/";
                } else {
                    $("#password-error span").text(response.responseJSON.errmsg);
                    $("#password-error").show();
                }
            }
        });
    });
});

タグ: flask Python javascript csrf werkzeug

Sun, 10 May 2026 08:43:05 +0900 投稿