HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

ユーザー操作制御機能(何)を作ってみる

前置き

最近、自分でコーディングする機会がめっきり減ってしまったので、ちょっとしたリハビリにちょっとしたものを作ってみる。
いい名前が思いつかなかったので、こんなタイトル(何)になってしまったが、要するに、

  • Windows(Vista以降(?))で言うところの、シールドアイコン付きボタン/メニュー/等々
  • Mac OS Xで言うところの、鍵アイコン

によって、ある操作を行いたいときには追加で認証を求めるという仕組み。

  • 普段は使わない/誤操作が怖い「編集」や「削除」ボタンなどを、普段は無効にしておくことで誤操作を防ぐ
  • 離席中に誰かが操作しようとしても、認証を求められるから安心

みたいな効果が期待されるのは、きっと周知の事実(謎)。

ざっくりとした構成

適当に設計してみる。

  • 機能ロック状態マネージャ
    • 機能IDごとに、最終ロック解除日時を保持する
    • 一定時間経過後に機能ロック状態を確認しようとしたら、当該データを破棄する
    • 最終ロック解除日時が保持されている機能は、ロック解除状態であると判断する
  • ロック解除API
    • POSTで、ユーザーID、パスワード、機能IDを送ると、認証した上で指定機能をロック解除状態にする
    • とりあえず固定のユーザーIDとパスワードを入力すればOKとする
    • ロック解除の成否をJSON形式で返す
  • UI
    • ロック解除ボタンを押すと、認証画面を表示
    • 認証はAjax

サーバー側の処理

まずは、機能ロック状態マネージャ。(ファイル名:UnlockManager.php)

<?php
    class UnlockManager {
        const UNLOCKED_SPAN = 300; // 5 minutes

        public static function setUnlocked($funcName) {
            self::sessionStart();
            $_SESSION['unlock'][$funcName] = array(
                "lastUnlocked" => time(),
                );
        }
        public static function isUnlocked($funcName) {
            self::sessionStart();
            if (isset($_SESSION['unlock'][$funcName]['lastUnlocked'])) {
                if ($_SESSION['unlock'][$funcName]['lastUnlocked'] + self::UNLOCKED_SPAN < time()) {
                    unset($_SESSION['unlock'][$funcName]);
                }
                return isset($_SESSION['unlock'][$funcName]['lastUnlocked']);
            } else {
                return false;
            }
        }
        public static function getLastUnlocked($funcName) {
            self::sessionStart();
            if (isset($_SESSION['unlock'][$funcName]['lastUnlocked'])) {
                return $_SESSION['unlock'][$funcName]['lastUnlocked'];
            } else {
                return false;
            }
        }
        private static function sessionStart() {
            $sid = session_id();
            if (empty($sid)) {
                session_start();
            }
        }
    }

次は、ロック解除API。(ファイル名:unlock.php)

<?php
    require_once('UnlockManager.php');

    if (isset($_POST['userId'], $_POST['password'], $_POST['func'])) {
        $userId = $_POST['userId'];
        $password = $_POST['password'];
        $func = $_POST['func'];

        // TODO 実際は、ちゃんとユーザー認証
        if ($userId == "admin" && $password == "admin") {
            UnlockManager::setUnlocked($func);
        }
        $res = array('unlocked' => UnlockManager::isUnlocked($func));
    } else if (isset($_POST['func'])) {
        $func = $_POST['func'];
        $res = array('unlocked' => UnlockManager::isUnlocked($func));
    } else {
        $res = array('unlocked' => false);
    }

    print(json_encode($res));

クライアント側の処理

処理を簡単に書くために、prototype.js(v1.7.0)を使用。
PHP用のテンプレートエンジンとかを使えばもうちょっときれいに書けただろうけど、とりあえずはPHPファイルの中にHTMLをべたっと。(ファイル名:app.php)

<?php
    require_once('UnlockManager.php');
?>
<html>
    <head>
        <title>Unlock test</title>
        <script type="text/javascript" src="prototype.js"></script>
        <script type="text/javascript">
        <!--
        function init() {
            Ajax.Responders.register({
                onComplete: function(req) {
                    var res = eval("(" + req.transport.responseText + ")");
                    if (res.unlocked) {
                        window.location.reload();
                    }
                }
            });
        }
        function auth(func) {
            $('form1').func.value = func;
            $('unlock').style.display = '';
            return false;
        }
        function setUnlocked() {
            $('unlock').style.display = 'none';
            new Ajax.Request('unlock.php', {
                parameters: $('form1').serialize(true)
            });
            return false;
        }
        //-->
        </script>
    </head>
    <body onload="init()">
        <div style="display:none" id="unlock">
            <form action="" method="post" name="form1" id="form1">
                <input type="text" name="userId" autocomplete="off" /><br />
                <input type="password" name="password" autocomplete="off" /><br />
                <input type="hidden" name="func" />
                <input type="button" value="OK" onclick="setUnlocked();" />
            </form>
        </div>
        <table>
<?php
    foreach (array('func_1', 'func_2', 'func_3') as $func) {
?>
            <tr>
                <th><?php print($func);?></th>
                <td>
<?php
    if (UnlockManager::isUnlocked($func)) {
?>
                    <input type="button" value="Do <?php print($func);?>" />
<?php
    } else {
?>
                    <input type="button" value="Do <?php print($func);?>" disabled="true" />
<?php
    }
?>
                </td>
                <td>
<?php
    if (UnlockManager::isUnlocked($func)) {
?>
                    [  ]
<?php
    } else {
?>
                    <a href="#" onclick="auth('<?php print($func);?>');">][</a>
<?php
    }
?>
                </td>
            </tr>
<?php
    }
?>
        </table>
        <hr />
        <h2>debug output</h2>
        <pre>
<?php
    print_r(array("time" => time(), "session" => $_SESSION['unlock']));
?>
        </pre>
    </body>
</html>

蛇足

app.php にアクセスし、"]["(扉が閉じている図のつもり)をクリックすると、認証用のフォームが表示される。
正しいユーザーID/パスワードを入力してOKをクリックすると、選択した機能のボタン("Do func_x")が押せるようになる。
5分以上経過した後にページをリロードすると、再び機能のボタンが押せなくなる。


管理者はいろんなことができちゃって怖いので、普段はこういう機能によって操作を制限されていると安心かもね。