開発情報・ナレッジ

投稿者: SPIRERS ナレッジ向上チーム 2025年11月20日 (木)

パスワードの世代管理を行い重複を防ぐ方法

本記事では同じパスワードを使い続けないように世代管理を行う方法をご紹介いたします。
過去のパスワードを5世代保存し、新しく入力されたパスワードと比較することで、
同じパスワードを使用していないかチェックします。
SPIRALではフィールドタイプがパスワードの場合、登録後の入力内容の呼び出しができないため
パスワードはハッシュ化して保存いたします。

全体図

イメージ
フロー図
ハッシュ化について

ハッシュ化とは、元のデータを「ハッシュ関数」と呼ばれる特殊な計算方法を用いて、
固定長で一見ランダムに見える「ハッシュ値」に変換する処理です。
このハッシュ値は、元のデータを復元することが極めて困難であり、同じデータからは常に
同じハッシュ値が生成されるという特徴を持ちます。
この性質を利用して、データの改ざん検知や、パスワードを安全に保存する際などに活用されます。

DB構成

パスワード登録時
DBの構成となります。
「パスワード」「パスワード履歴1~5」を今回の世代管理に使用しています。

▼ ログインDB
表示名 識別名 タイプ 必須・重複
メールアドレス email テキスト 入力必須かつ重複不可
パスワード password パスワード -
パスワード(ハッシュ処理用) passwordH テキストエリア -
パスワード履歴1 password1 テキストエリア -
パスワード履歴2 password2 テキストエリア -
パスワード履歴3 password3 テキストエリア -
パスワード履歴4 password4 テキストエリア -
パスワード履歴5 password5 テキストエリア -

設定方法

パスワード登録時

新規登録フォームブロックからパスワードを新規登録した際の完了ステップにて
APIでハッシュ化したパスワードを更新します。


PHP
<?php

//------------------------------
// 設定値
//------------------------------
define("API_URL", "https://api.spiral-platform.com/v1");
define("API_KEY", "XXXXX");
define("APP_ROLE", "");
define("APP_ID", "XXXXX");
define("DB_ID", "XXXXX");
define("FORM_name", "XXXXX");//フォームブロックの識別名
define("passwordName", "XXXXX");//パスワードの識別名
define("password1Name", "XXXXX");//パスワード履歴1の識別名

//------------------------------
// パスワードをハッシュ化して更新
//------------------------------
$registForm = $SPIRAL->getRegistrationForm(FORM_name);

// 完了ステップでのみ実行する
if ($registForm->isCompletedStep()) { 
	$record = $SPIRAL->getRecordValue(); 
	$recordId = $record['item']['_id']; //更新対象レコードID
    $password = $SPIRAL->getParam(passwordName);

    // 値が空でなければハッシュ化(SHA-256)
    if ($password !== '') {
        $password1 = hash('sha256', $password);
    } else {
        $password1 = '';
    }
    
    //------------------------------
    // API実行
    //------------------------------
    $commonBase = CommonBase::getInstance();

    // 更新するデータを指定
    $UpdateData = array(
        password1Name  => $password1,
    );

    $resultRecordUpdate = $commonBase->apiCurlAction("PATCH", "/apps/". APP_ID. "/dbs/". DB_ID. "/records/". $recordId, $UpdateData);

}

    //------------------------------
    // 共通モジュール
    //------------------------------
    class CommonBase {
        /**
        * シングルトンインスタンス
        * @var UserManager
        */
        protected static $singleton;

        public function __construct() {
            if (self::$singleton) {
                throw new Exception('must be singleton');
            }
            self::$singleton = $this;
        }
        /**
        * シングルトンインスタンスを返す
        * @return UserManager
        */
        public static function getInstance() {
            if (!self::$singleton) {
                return new CommonBase();
            } else {
                return self::$singleton;
            }
        }
        /**
        * V2用 API送信ロジック
        * @return Result
        */
        function apiCurlAction($method, $addUrlPass, $data = null, $multiPart = null, $jsonDecode = null) {
            $header = array(
                "Authorization:Bearer ". API_KEY,
                "X-Spiral-Api-Version: 1.1",
            );
            if($multiPart) {
                $header = array_merge($header, array($multiPart));
            } else {
                $header = array_merge($header, array("Content-Type:application/json"));
            }
            if(APP_ROLE){
                $header = array_merge($header, array("X-Spiral-App-Role: ".APP_ROLE));
            }
            // curl
            $curl = curl_init();
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($curl, CURLOPT_URL, API_URL. $addUrlPass);
            curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
            if ($method == "POST") {
                if ($multiPart) {
                    curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
                } else {
                    curl_setopt($curl, CURLOPT_POSTFIELDS , json_encode($data));
                }
                curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
            }
            if ($method == "PATCH") {
                curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
                curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
            }
            if ($method == "DELETE") {
                curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
                curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
            }
            $response = curl_exec($curl);
            if (curl_errno($curl)) echo curl_error($curl);
            curl_close($curl);
            if($jsonDecode){
                return $response;
            }else{
                return json_decode($response, true);
            }
        }
    }
?>
設定値
API_URL 6行目 リクエスト先URLの固定部分です。
固定値ですので特に変更する必要はありません。
API_KEY 7行目 発行したAPIキーを設定してださい。
別途権限の付与が必要になります。
APIKEYの発行、確認場所
APP_ROLE 8行目 設定したアプリロールの識別名を入れてください。
全権限の場合は値は空で大丈夫です。
アプリロールについて
APP_ID 9行目 レコード操作を行うDBがあるアプリのIDを設定してください。
アプリIDの確認場所
DB_ID 10行目 レコード操作を行うDBのIDを設定してください。
DBIDの確認場所
FORM_name 11行目 パスワード登録を行う登録フォームブロックの識別名を設定してください。
識別名は登録フォームブロックの基本設定から確認可能です。
passwordName 12行目 フィールド名「パスワード」のname属性を設定してください。
name属性は登録フォームブロックのフィールド一覧ボタンから確認可能です。
※ デフォルトでは「f0X」の「X」がIDとなります。
password1Name 13行目 フィールド名「パスワード履歴1」のname属性を設定してください。
name属性は登録フォームブロックのフィールド一覧ボタンから確認可能です。
※ デフォルトでは「f0X」の「X」がIDとなります。
パスワード更新時

javaScriptでパスワード入力時に入力したパスワードと過去のパスワードの比較を行います。
履歴の中に同じパスワードが含まれていた場合はエラーメッセージを表示して、
送信ボタンを非表示に変更します。
同じパスワードが含まれない場合は送信ボタンを表示します。
また、パスワード更新した際の完了ステップにてAPIでパスワード履歴を更新します。

HTML
<sp:input-field name="f0X"></sp:input-field>
<input type="hidden" th:name="${fields['f0X'].name}" id="passwordH" th:value="${inputs['f0X']}">
<input type="hidden" name="passwordLOG1" id="passwordLOG1" th:value="${siteClient.record[X]}">
<input type="hidden" name="passwordLOG2" id="passwordLOG2" th:value="${siteClient.record[X]}">
<input type="hidden" name="passwordLOG3" id="passwordLOG3" th:value="${siteClient.record[X]}">
<input type="hidden" name="passwordLOG4" id="passwordLOG4" th:value="${siteClient.record[X]}">
<input type="hidden" name="passwordLOG5" id="passwordLOG5" th:value="${siteClient.record[X]}">
※ ソース設定でブロックに設定してください。
変更点
sp:input-field name="f0X" 1行目 「X」はフィールド名「パスワード(ハッシュ処理用)」のIDを設定してください。
th:name="${fields['f0X'].name}"
id="passwordH" th:value="${inputs['f0X']}"
2行目 「X」はフィールド名「パスワード(ハッシュ処理用)」のIDを設定してください。
${siteClient.record[X]} 3行目 「X」はフィールド名「パスワード履歴1」のIDを設定してください。
${siteClient.record[X]} 4行目 「X」はフィールド名「パスワード履歴2」のIDを設定してください。
${siteClient.record[X]} 5行目 「X」はフィールド名「パスワード履歴3」のIDを設定してください。
${siteClient.record[X]} 6行目 「X」はフィールド名「パスワード履歴4」のIDを設定してください。
${siteClient.record[X]} 7行目 「X」はフィールド名「パスワード履歴5」のIDを設定してください。
※ フィールドのIDはアプリ管理<DB<確認したいフィールドの表示名をクリックすることで確認可能です。

javaScript
// ▼ SHA-256 ハッシュ化関数
async function sha256Hex(text) {
  const data = new TextEncoder().encode(text);
  const hashBuffer = await crypto.subtle.digest('SHA-256', data);
  return Array.from(new Uint8Array(hashBuffer))
              .map(b => b.toString(16).padStart(2, '0'))
              .join('');
}

// ▼ DOM準備後にイベント登録
document.addEventListener('DOMContentLoaded', function() {
  const pwInputField = document.querySelector('input[name="XXXX"]');//パスワードの識別名
  const pwHname = "XXXX";//パスワード(ハッシュ処理用)の識別名
  const submitButton = document.querySelector('.sp-form-next-button');
  if (!pwInputField || !submitButton) return;

  pwInputField.addEventListener('input', async function() {
    const pwInput = pwInputField.value.trim();
    if (!pwInput) {
      submitButton.style.display = ''; // パスワード未入力時はボタン表示
      return;
    }

    // ハッシュ化
    const hashed = (await sha256Hex(pwInput)).toLowerCase();
    document.getElementById(pwHname).value = hashed;

    // 履歴取得(フォーム外)
    const logs = [];
    for (let i = 1; i <= 5; i++) {
      const el = document.getElementById(`passwordLOG${i}`);
      if (el && el.value) logs.push(el.value.toLowerCase().trim());
    }

    // 重複チェック
    const isDuplicate = logs.includes(hashed);

    if (isDuplicate) {
      alert("過去に使用したパスワードと一致しています。別のパスワードを入力してください。");
      submitButton.style.display = 'none'; // エラー時はボタン非表示
    } else {
      submitButton.style.display = ''; // 問題なければボタン表示
    }
  });
});
変更点
('input[name="XXXX"]'); 12行目 フィールド名「パスワード」の識別名を設定してください。
name属性は登録フォームブロックのフィールド一覧ボタンから確認可能です。
pwHname = "XXXX"; 13行目 フィールド名「パスワード(ハッシュ処理用)」の識別名を設定してください。
name属性は登録フォームブロックのフィールド一覧ボタンから確認可能です。

PHP
<?php

//------------------------------
// 設定値
//------------------------------
define("API_URL", "https://api.spiral-platform.com/v1");
define("API_KEY", "XXXXX");
define("APP_ROLE", "");
define("APP_ID", "XXXXX");
define("DB_ID", "XXXXX");
define("FORM_name", "XXXXX");//フォームブロックの識別名
define("passwordHName", "XXXXX");//パスワード(ハッシュ処理用)の識別名
define("password1Name", "XXXXX");//パスワード履歴1の識別名
define("password2Name", "XXXXX");//パスワード履歴2の識別名
define("password3Name", "XXXXX");//パスワード履歴3の識別名
define("password4Name", "XXXXX");//パスワード履歴4の識別名
define("password5Name", "XXXXX");//パスワード履歴5の識別名

//------------------------------
// パスワードをハッシュ化して更新
//------------------------------
$updateForm = $SPIRAL->getUpdateForm(FORM_name);
$SPIRAL->setTHValue("updateForm",$updateForm->isCompletedStep());

// 完了ステップでのみ実行する
if ($updateForm->isCompletedStep()) { 
	$record = $SPIRAL->getRecordValue(); 
	$recordId = $record['item']['_id']; //更新対象レコードID
	$passwordH = $record['item'][passwordHName]; //パスワード(ハッシュ処理用)
	$password_OLD1 = $record['item'][password1Name]; //更新前のパスワード履歴1
	$password_OLD2 = $record['item'][password2Name]; //更新前のパスワード履歴2
	$password_OLD3 = $record['item'][password3Name]; //更新前のパスワード履歴3
	$password_OLD4 = $record['item'][password4Name]; //更新前のパスワード履歴4

    
    //------------------------------
    // API実行
    //------------------------------
    $commonBase = CommonBase::getInstance();

    // 更新するデータを指定
    $UpdateData = array(
        password1Name  => $passwordH,
        password2Name  => $password_OLD1,
        password3Name  => $password_OLD2,
        password4Name  => $password_OLD3,
        password5Name  => $password_OLD4,
    );

    $resultRecordUpdate = $commonBase->apiCurlAction("PATCH", "/apps/". APP_ID. "/dbs/". DB_ID. "/records/". $recordId, $UpdateData);

}

    //------------------------------
    // 共通モジュール
    //------------------------------
    class CommonBase {
        /**
        * シングルトンインスタンス
        * @var UserManager
        */
        protected static $singleton;

        public function __construct() {
            if (self::$singleton) {
                throw new Exception('must be singleton');
            }
            self::$singleton = $this;
        }
        /**
        * シングルトンインスタンスを返す
        * @return UserManager
        */
        public static function getInstance() {
            if (!self::$singleton) {
                return new CommonBase();
            } else {
                return self::$singleton;
            }
        }
        /**
        * V2用 API送信ロジック
        * @return Result
        */
        function apiCurlAction($method, $addUrlPass, $data = null, $multiPart = null, $jsonDecode = null) {
            $header = array(
                "Authorization:Bearer ". API_KEY,
                "X-Spiral-Api-Version: 1.1",
            );
            if($multiPart) {
                $header = array_merge($header, array($multiPart));
            } else {
                $header = array_merge($header, array("Content-Type:application/json"));
            }
            if(APP_ROLE){
                $header = array_merge($header, array("X-Spiral-App-Role: ".APP_ROLE));
            }
            // curl
            $curl = curl_init();
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($curl, CURLOPT_URL, API_URL. $addUrlPass);
            curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
            if ($method == "POST") {
                if ($multiPart) {
                    curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
                } else {
                    curl_setopt($curl, CURLOPT_POSTFIELDS , json_encode($data));
                }
                curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
            }
            if ($method == "PATCH") {
                curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
                curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
            }
            if ($method == "DELETE") {
                curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
                curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
            }
            $response = curl_exec($curl);
            if (curl_errno($curl)) echo curl_error($curl);
            curl_close($curl);
            if($jsonDecode){
                return $response;
            }else{
                return json_decode($response, true);
            }
        }
    }
?>
設定値
API_URL 6行目 リクエスト先URLの固定部分です。
固定値ですので特に変更する必要はありません。
API_KEY 7行目 発行したAPIキーを設定してださい。
別途権限の付与が必要になります。
APIKEYの発行、確認場所
APP_ROLE 8行目 設定したアプリロールの識別名を入れてください。
全権限の場合は値は空で大丈夫です。
アプリロールについて
APP_ID 9行目 レコード操作を行うDBがあるアプリのIDを設定してください。
アプリIDの確認場所
DB_ID 10行目 レコード操作を行うDBのIDを設定してください。
DBIDの確認場所
FORM_name 11行目 パスワード更新を行う更新フォームブロックの識別名を設定してください。
識別名は更新フォームブロックの基本設定から確認可能です。
passwordHName 12行目 フィールド名「パスワード(ハッシュ処理用)」の識別名を設定してください。
name属性は更新フォームブロックのフィールド一覧ボタンから確認可能です。
password1Name 13行目~17行目 フィールド名「パスワード履歴1~5」の識別名を設定してください。
name属性は更新フォームブロックのフィールド一覧ボタンから確認可能です。

まとめ

パスワードのデータを管理するにあたり、
世代管理がシステムの要件の一つだったりする場合はぜひこの活用法をご参考ください。

  

解決しない場合はこちら コンテンツに関しての
要望はこちら