実装前に理解しておくべき重要ポイント
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": メッセージ}
サーバーサイドの処理フロー
- パラメータの受領(電話番号、認証コード、パスワード)
- 入力値のバリデーション(全体チェック、電話番号フォーマット検証)
- RedisからSMS認証コードを取得し、有効期限と一致性を確認
- 認証成功後、Redisから認証コードを削除
- 新規ユーザーを作成してデータベースに保存
- セッションにユーザー情報を設定
バックエンド実装
# -*- 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": メッセージ}
サーバーサイドの処理フロー
- パラメータの受領(電話番号、パスワード)
- パラメータのバリデーション
- クライアントIPアドレスを取得
- Redisからログイン失敗回数をチェック
- 5回以上の失敗がある場合はブロック
- ユーザー認証(電話番号とパスワードの照合)
- 認証成功時にRedisの失敗カウンタをリセット
- セッション情報の設定
- 結果を返す
バックエンド実装
@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();
}
}
});
});
});