Ajaxログイン認証 公開ソースコード
開発/実行環境
- OS
- Windows 7
- サーバー言語
- PHP
- 言語バージョン
- ローカル 5.38
リモート 5.3.15 - 実行環境
- Xampp 1.7.7
- 開発環境
- Dreamweaver CS5.5
- ローカルサーバー
- Apache
- RDBS
- MySQL 5.5.16
ディレクトリ・ファイル構成
root(https://digick-wiz-code.ssl-lolipop.jp/) │ │ └─dev │ ├─lib │ jquery │ bootstrap │ underscore │ ├─js │ FSM-0.6.js │ └─ajax-login ──────────────────────────────── ajax-login │ │ .htaccess │ │ config.php │ functions.php │ │ index.php │ login.php │ logout.php │ register.php │ ├─css │ ajax-login.css │ ├─js │ ajax-login.js │ └─tpl admin-frag.inc.php ajax-login.inc.php auth-frag.inc.php
データベースの構成
- データベース名
- ajax_login
- エンコーディング/照合順序
- utf8_general_ci
カラム名 | 種別 | ヌル(NULL) | デフォルト値 | その他 |
---|---|---|---|---|
id | varchar(255) | いいえ | なし | 主キー |
varchar(255) | はい | NULL | ||
password | varchar(255) | はい | NULL | |
type | varchar(255) | はい | NULL | |
create_time | datetime | はい | NULL | |
update_time | datetime | はい | NULL | |
last_pass_change_time | datetime | はい | NULL |
ステートマシン図
php.iniの設定(ローカル)
セッション関係
session.use_cookies = 1
session.use_only_cookies = 1
session.use_trans_sid = 0
session.gc_probability = 1
session.gc_divisor = 100
session.gc_maxlifetime = 3600
エンコーディング関係
mbstring.language = Japanese
mbstring.internal_encoding = UTF-8
mbstring.http_input = pass
mbstring.http_output = pass
mbstring.encoding_translation = Off
mbstring.detect_order = UTF-8,SJIS,EUC-JP,JIS,ASCII
mbstring.substitute_character = none
mbstring.func_overload = 0
mbstring.strict_detection = Off
ソースコード
- config.php
- functions.php
- index.php
- register.php
- login.php
- logout.php
- ajax-login.inc.php
- auth-frag.inc.php
- admin-frag.inc.php
- ajax-login.js
- ajax-login.css
config.php
<?php $dbname = 'ajax_login'; $host = '127.0.0.1'; $dsn = "mysql:dbname={$dbname};host={$host};charset=utf8"; $username = 'root'; $password = 'wizard1977'; $driver_options = array( PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => false, ); ?>
functions.php
<?php /* FIXEDSALTに暗号論的擬似乱数生成器関数を使用 */ //echo bin2hex(openssl_random_pseudo_bytes(20)); define('FIXEDSALT', '5dd7599454e8068c0e740710fa50861711c65c94'); define('STRETCH_COUNT', 1000); function get_salt($id) { return $id.pack('H*', FIXEDSALT); } function get_password_hash($id, $pwd) { $salt = get_salt($id); $hash = ''; for ($i = 0; $i < STRETCH_COUNT; $i++) { $hash = hash('sha256', $hash.$pwd.$salt); } return $hash; } function json_safe_encode($val) { return json_encode($val, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); } function h($str) { return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); } ?>
index.php
<?php require_once './functions.php'; /* クッキーに保存されるセッションIDの設定 */ session_set_cookie_params(60 * 60, '/', '', TRUE, TRUE); //期間は60分。HttpOnlyかつSecure属性はTRUE session_start(); if (!isset($_SESSION['user_id'])) { /* ユーザーのログイン/登録手続き用のCSRF対策トークン */ $token = session_id(); /* ログイン/登録画面のコンポーネントを先に変数に入れる */ ob_start(); include_once 'tpl/auth-frag.inc.php'; $template_frag = ob_get_contents(); ob_end_clean(); /* 後からページのコンテナ部分を読み込む */ include_once 'tpl/ajax-login.inc.php'; } else { /* 認証済みの場合、セッションIDを振り直す */ session_regenerate_id(TRUE); $user_id = $_SESSION['user_id']; $email = $_SESSION['email']; /* ユーザーの何らかの手続き用のCSRF対策トークン */ $token = session_id(); /* ユーザー管理画面のコンポーネントを変数に入れる */ ob_start(); include_once 'tpl/admin-frag.inc.php'; $template_frag = ob_get_contents(); ob_end_clean(); /* 後からページのコンテナ部分を読み込む */ include_once 'tpl/ajax-login.inc.php'; } ?>
register.php
<?php require_once './config.php'; require_once './functions.php'; /* クッキーに保存されるセッションIDの設定 */ session_set_cookie_params(60 * 60, '/', '', TRUE, TRUE); header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); $response = array(); session_start(); /* セッションがタイムアウトしていたら手続きを終了 */ if (!isset($_COOKIE[session_name()])) { $response['message'] = 'expired'; echo json_safe_encode($response); exit; } /* Ajax以外のリクエストだったら強制終了 */ if (@$_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') { $response['message'] = 'not XHR'; echo json_safe_encode($response); exit; } $user_id = $_POST['user-id']; $email = $_POST['email']; $pass = $_POST['password']; $regi_token = $_POST['regi-token']; /* トークンが異なっていたら強制終了 */ if (session_id() !== $regi_token) { $response['message'] = 'illicit access'; echo json_safe_encode($response); exit; } /* サーバーサイドでも入力値の検証を行う */ if (preg_match('/\A[a-zA-Z0-9_]{4,20}\z/u', $user_id) !== 1) { $response['message'] = 'invalid userId'; echo json_safe_encode($response); exit; } if (preg_match('/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/ui', $email) !== 1) { $response['message'] = 'invalid email'; echo json_safe_encode($response); exit; } if (preg_match('/\A(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[a-zA-Z\d]{8,16}+\z/u', $pass) !== 1) { $response['message'] = 'invalid password'; echo json_safe_encode($response); exit; } try { /* データベース接続 */ $pdo = new PDO($dsn, $username, $password, $driver_options); $sql = "SELECT * FROM account WHERE id=:id"; $stmt = $pdo->prepare($sql); $stmt->bindParam(':id', $user_id, PDO::PARAM_STR); $stmt->execute(); $user_id_duplicated = $stmt->fetch(); $sql = "SELECT * FROM account WHERE email=:email"; $stmt = $pdo->prepare($sql); $stmt->bindParam(':email', $email, PDO::PARAM_STR); $stmt->execute(); $email_duplicated = $stmt->fetch(); if (empty($user_id_duplicated) && empty($email_duplicated)) { /* 重複登録がないことを確認。登録処理へ */ $time = date('Y-m-d H:i:s'); $hash = get_password_hash($user_id, $pass); $sql = "INSERT INTO account (id, email, password, type, create_time, update_time, last_pass_change_time) VALUES(:id, :email, :pass, :type, :create_time, :update_time, :last_pass_change_time)"; $stmt = $pdo->prepare($sql); $stmt->bindParam(':id', $user_id, PDO::PARAM_STR); $stmt->bindParam(':email', $email, PDO::PARAM_STR); $stmt->bindParam(':pass', $hash, PDO::PARAM_STR); $stmt->bindValue(':type', 'general', PDO::PARAM_STR); $stmt->bindParam(':create_time', $time, PDO::PARAM_STR); $stmt->bindParam(':update_time', $time, PDO::PARAM_STR); $stmt->bindParam(':last_pass_change_time', $time, PDO::PARAM_STR); $stmt->execute(); /* 認証状態に移行 */ session_regenerate_id(TRUE); $_SESSION['user_id'] = $user_id; $_SESSION['email'] = $email; /* ユーザーが何らかの手続きを行うときのCSRF対策トークン */ $token = session_id(); /* レスポンスをJSON形式で作成 */ $response['message'] = 'success'; $response['id'] = h($user_id); /* 認証後のページのHTMLデータを変数に入れる */ ob_start(); include_once 'tpl/admin-frag.inc.php'; $template_frag = ob_get_contents(); ob_end_clean(); $response['html'] = $template_frag; } else { if (!empty($user_id_duplicated)) { $response['message'] = 'username duplicated'; } else { $response['message'] = 'email duplicated'; } } echo json_safe_encode($response); } catch (PDOException $e) { $response['message'] = $e->getMessage(); echo json_safe_encode($response); exit; } ?>
login.php
<?php require_once './config.php'; require_once './functions.php'; /* クッキーに保存されるセッションIDの設定 */ session_set_cookie_params(60 * 60, '/', '', TRUE, TRUE); header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); $response = array(); session_start(); /* セッションがタイムアウトしていたら手続きを終了 */ if (!isset($_COOKIE[session_name()])) { $response['message'] = 'expired'; echo json_safe_encode($response); exit; } /* Ajax以外のリクエストだったら強制終了 */ if (@$_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') { $response['message'] = 'not XHR'; echo json_safe_encode($response); exit; } $email = $_POST['email']; $pass = $_POST['password']; $login_token = $_POST['login-token']; /* トークンが異なっていたら強制終了 */ if (session_id() !== $login_token) { $response['message'] = 'illicit access'; echo json_safe_encode($response); exit; } /* サーバーサイドでも入力値の検証を行う */ if (preg_match('/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/ui', $email) !== 1) { $response['message'] = 'invalid email'; echo json_safe_encode($response); exit; } if (preg_match('/\A(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[a-zA-Z\d]{8,16}+\z/u', $pass) !== 1) { $response['message'] = 'invalid password'; echo json_safe_encode($response); exit; } try { $pdo = new PDO($dsn, $username, $password, $driver_options); $sql = "SELECT * FROM account WHERE email=:email"; $stmt = $pdo->prepare($sql); $stmt->bindParam(':email', $email, PDO::PARAM_STR); $stmt->execute(); $account = $stmt->fetch(); if (!empty($account)) { $hash = get_password_hash($account['id'], $pass); if ($account['password'] === $hash) { $time = date('Y-m-d H:i:s'); $user_id = $account['id']; $sql = "UPDATE account SET update_time=:update_time WHERE id=:id"; $stmt = $pdo->prepare($sql); $stmt->bindParam(':update_time', $time, PDO::PARAM_STR); $stmt->bindParam(':id', $user_id, PDO::PARAM_STR); $stmt->execute(); /* 認証状態に移行 */ session_regenerate_id(TRUE); $_SESSION['user_id'] = $user_id; $_SESSION['email'] = $email; /* ユーザーが何らかの手続きを行うときのCSRF対策トークン */ $token = session_id(); /* レスポンスをJSON形式で作成 */ $response['message'] = 'success'; $response['id'] = h($user_id); /* 認証後のページのHTMLデータを変数に入れる */ ob_start(); include_once 'tpl/admin-frag.inc.php'; $template_frag = ob_get_contents(); ob_end_clean(); $response['html'] = $template_frag; } else { $response['message'] = 'failure'; } } else { $response['message'] = 'failure'; } echo json_safe_encode($response); } catch (PDOException $e) { $response['message'] = $e->getMessage(); echo json_safe_encode($response); exit; } ?>
logout.php
<?php require_once './functions.php'; header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); $response = array(); if (@$_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') { $response['message'] = 'not XHR'; echo json_safe_encode($response); die(); } $logout_token = $_POST['logout-token']; session_start(); /* トークンが異なっていたら強制終了 */ if (session_id() !== $logout_token) { $response['message'] = 'illicit access'; echo json_safe_encode($response); die(); } $response['message'] = 'success'; /* 念のためセッションIDを格納しているCookieを削除 */ setcookie(session_name(), '', time() - 3600, '/'); /* セッションデータを削除し、セッションを終了する */ $_SESSION = array(); session_destroy(); echo json_safe_encode($response); ?>
ajax-login.inc.php
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="Description" content="JavaScriptやAjaxライブラリを利用したゲームなどを公開しています。"> <meta name="Keywords" content="Ajax,JavaScript,HTML5,jQuery,Bootstrap,ゲーム"> <meta name="author" content="masa"> <meta name="copyright" content="Copyright © 2009 WIZARD-CODE"> <title>Ajaxログイン認証 - ウィザード・コード | WIZARD-CODE</title> <link rel="shortcut icon" href="//wiz-code.digick.jp/image/icon/favicon.ico"> <link rel="copyright" href="//wiz-code.digick.jp/"> <link href="/lib/bootstrap/3.3.1/css/bootstrap.css" rel="stylesheet"> <link href="/test/ajax-login/css/ajax-login.css" rel="stylesheet"> </head> <body> <div class="container-fluid"> <div id="ajax-login" class="panel panel-default"><?=$template_frag?></div> </div> <script src="/lib/jquery/jquery-1.10.1.js"></script> <script src="/lib/bootstrap/3.3.1/js/bootstrap.js"></script> <script src="/lib/underscore/1.7.0/underscore.js"></script> <script src="/js/FSM-0.6.js"></script> <script src="/test/ajax-login/js/ajax-login.js"></script> </body> </html>
auth-frag.inc.php
<div id="content" class="panel-body" data-authentication-status="unauthenticated"> <h1 id="title" class="text-center">Ajaxログイン認証</h1> <div id="auth" class="text-center hidden"> <button id="register-button" type="button" class="btn btn-primary">登録する</button> <button id="login-button" type="button" class="btn btn-default">ログインする</button> </div> <div id="return-home-button" class="hidden"><button type="button" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-home"></span></button></div> <div id="validation" class="text-danger bg-danger hidden"></div> <div id="register" class="hidden"> <form class="form-horizontal"> <fieldset> <div class="form-group form-group-xs"> <label for="user-id" class="col-xs-offset-1 col-xs-4 control-label">登録ユーザー名</label> <div class="col-xs-4"> <input type="text" class="form-control user-id" name="user-id" placeholder="登録ユーザー名"> </div> </div> <div class="form-group form-group-xs"> <label for="email" class="col-xs-offset-1 col-xs-4 control-label">登録Eメール</label> <div class="col-xs-4"> <input type="email" class="form-control email" name="email" placeholder="登録Eメール"> </div> </div> <div class="form-group form-group-xs"> <label for="password" class="col-xs-offset-1 col-xs-4 control-label">登録パスワード</label> <div class="col-xs-4"> <input type="password" class="form-control password" name="password" placeholder="登録パスワード"> </div> </div> <input id="regi-token" name="regi-token" value="<?=h($token)?>" type="hidden"> <div class="form-group form-group-xs"> <div class="col-xs-offset-5 col-xs-4"> <button type="submit" class="btn btn-primary btn-sm">登録する</button> </div> </div> </fieldset> </form> </div> <div id="login" class="hidden"> <div class="row"> <div class="col-xs-offset-4 col-xs-4"> <form> <fieldset> <div class="form-group form-group-xs"> <label for="email">Eメール</label> <input type="email" class="form-control email" name="email" placeholder="Eメール"> </div> <div class="form-group form-group-xs"> <label for="password">パスワード</label> <input type="password" class="form-control password" name="password" placeholder="パスワード"> </div> <input id="login-token" name="login-token" value="<?=h($token)?>" type="hidden"> <button type="submit" class="btn btn-primary btn-sm">サインイン</button> </fieldset> </form> </div> </div> </div> <div id="credit" class="text-center"> <address>Copyright © WIZARD-CODE 2015</address> </div> </div>
admin-frag.inc.php
<div id="content" class="panel-body" data-authentication-status="authenticated"> <h1 id="title" class="text-center">こんにちは! <?=h($user_id)?>さん!</h1> <div id="validation" class="text-danger bg-danger hidden"></div> <div id="return-home-button" class="hidden"><button type="button" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-home"></span></button></div> <div id="menu" class="hidden"> <button id="chat" type="button" class="btn btn-default btn-block disabled">チャットする</button> <button id="find-friends" type="button" class="btn btn-default btn-block disabled">フレンドを見つける</button> <button id="setting-button" type="button" class="btn btn-default btn-block">設定</button> <button id="logout-button" type="button" class="btn btn-default btn-block">ログアウトする</button> </div> <div id="logout" class="hidden"> <div class="alert alert-warning text-center" role="alert"> <h4 class="text-center">ログアウトの確認</h4> <p>「ログアウトする」をクリックするとログアウト処理が完了します。</p> <form> <input id="logout-token" name="logout-token" value="<?=h($token)?>" type="hidden"> <button id="confirm-logout-button" type="button" class="btn btn-warning">ログアウトする</button> <button id="return-menu-from-logout" type="button" class="btn btn-default">戻る</button> </form> </div> </div> <div id="setting" class="hidden"> <form> <fieldset> <input id="setting-token" name="setting-token" value="<?=h($token)?>" type="hidden"> <table class="table table-condensed"> <tbody> <tr> <th class="col-md-4">ユーザー名</th> <td id="user-name" colspan="2" class="col-md-8"><?=h($user_id)?></td> </tr> <tr> <th class="col-md-4">Eメールアドレス</th> <td id="email" class="col-md-6"><?=h($email)?></td> <td class="col-md-2"><button id="change-email" type="button" class="btn btn-success btn-sm">変更</button></td> </tr> <tr class="hidden"> <th>Eメール変更</th> <td class="text-right"> <label class="sr-only" for="new-email">Eメール</label> <input type="email" class="form-control input-sm" id="new-email" name="new-email" placeholder="Eメール"> </td> <td> <button type="submit" class="btn btn-primary btn-sm">送信</button> </td> </tr> <tr> <th colspan="2">パスワード変更</th> <td><button id="change-email" type="button" class="btn btn-success btn-sm">変更</button></td> </tr> <tr class="hidden"> <th>パスワード変更</th> <td> <label class="sr-only" for="new-password">新しいパスワード</label> <input type="email" class="form-control input-sm" id="new-password" name="new-password" placeholder="新しいパスワード"> </td> <td></td> </tr> <tr class="hidden"> <th class="delete-border-top"></th> <td class="delete-border-top"> <label class="sr-only" for="confirm-password">パスワードの再入力</label> <input type="email" class="form-control input-sm" id="confirm-password" placeholder="パスワードの再入力"> </td> <td class="delete-border-top"> <button type="submit" class="btn btn-primary btn-sm">送信</button> </td> </tr> <tr> <th colspan="2" class="insert-border-bottom">アカウント管理</th> <td class="insert-border-bottom"><button id="delete-account" type="button" class="btn btn-warning btn-sm">削除</button></td> </tr> <tr class="danger hidden"> <th>アカウントの削除</th> <td>アカウントを削除します。よろしいですか?</td> <td> <button type="submit" class="btn btn-danger btn-sm">送信</button> </td> </tr> </tbody> </table> </fieldset> </form> </div> <div id="credit" class="text-center"> <address>Copyright © WIZARD-CODE 2015</address> </div> </div>
ajax-login.js
(function () { 'use strict'; var fsm, state, transit, user, compo, button, message, FORM_FADE_SPEED; /* ユーザーデータを集約するオブジェクト */ user = {}; /* コンポーネント要素を集約するオブジェクト */ compo = {}; /* ボタン要素を集約するオブジェクト */ button = {}; /* メッセージ表示要素を集約するオブジェクト */ message = {}; /* 画面の表示アニメーション速度 */ FORM_FADE_SPEED = 200; /* Ajaxログインシステム */ fsm = {}; state = {}; transit = {}; fsm.ajaxLogin = new FSM('ajax-login'); state.branch = new State('branch'); fsm.ajaxLogin.addStateAsChoicePseudo(state.branch, function () { /* アプリケーション画面のフレーム */ compo.ajaxLogin = $('#ajax-login'); user.authentication = $('#content').attr('data-authentication-status'); if (user.authentication !== 'authenticated') { return 'auth-init'; } else { return 'admin-init'; } }); transit.firstTransit = new Transition('first-transit', null, 'branch'); fsm.ajaxLogin.addTransition(transit.firstTransit); /* ログイン/登録に関するステート群 */ state.authInit = new State('auth-init', { autoTransition: true, entryAction: function () { /* コンポーネント要素 */ compo.auth = $('#auth'); compo.register = $('#register'); compo.login = $('#login'); /* バリデーションのテキスト表示用要素 */ message.validation = $('#validation'); /* 最初の画面に戻るためのボタン */ button.returnHome = $('#return-home-button'); /* いったんボタンやコンポーネントを非表示に */ compo.auth.hide().removeClass('hidden'); compo.register.hide().removeClass('hidden'); compo.login.hide().removeClass('hidden'); message.validation.hide().removeClass('hidden'); button.returnHome.hide().removeClass('hidden'); } }); fsm.ajaxLogin.addState(state.authInit); /* 認証画面の設定 */ state.auth = new State('auth', { entryAction: function () { /* 認証画面を表示 */ compo.auth.show(); /* ボタンごとに遷移先を指定する */ compo.auth.find('button') .on('click', function (e) { var target; e.preventDefault(); e.stopPropagation(); $(this).blur(); target = e.target; if (target.id === 'login-button') { transit.authToLogin.trigger(); } else if (target.id === 'register-button') { transit.authToRegister.trigger(); } }); }, exitAction: function () { /* 画面遷移の際にイベントリスナーを削除 */ compo.auth.find('button').off('click').end().hide(); } }); fsm.ajaxLogin.addState(state.auth); transit.authInitToAuth = new Transition('auth-init-to-auth', 'auth-init', 'auth'); fsm.ajaxLogin.addTransition(transit.authInitToAuth); /* 登録画面の設定 */ state.register = new State('register', { entryAction: function () { /* 登録画面を表示 */ compo.register.show(); /* 最初の画面へ戻るボタンを表示 */ button.returnHome.show() .on('click', function (e) { /* 登録画面から最初の画面へ戻る */ transit.registerToAuth.trigger(); }); /* 「登録する」ボタンを押した際の処理 */ compo.register.find('button').on('click', function (e) { var form, userId, email, password, query; e.preventDefault(); e.stopPropagation(); $(this).blur(); form = compo.register.find('form'); userId = form.find('.user-id'); email = form.find('.email'); password = form.find('.password'); message.validation.text('').hide(); /* 入力値をトリミングする */ userId.val($.trim(userId.val())); email.val($.trim(email.val())); password.val($.trim(password.val())); /* ユーザー名のバリデーション */ if (!/^[a-zA-Z0-9_]{4,20}$/.test(userId.val())) { userId.val(''); message.validation.text('ユーザー名は半角英数字かアンダースコア(_)のみ使用できます。4~20文字の範囲で入力してください。').show(FORM_FADE_SPEED); return; } /* メールアドレスのバリデーション */ if (!/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/i.test(email.val())) { email.val(''); message.validation.text('メールアドレスが正しくありません。').show(FORM_FADE_SPEED); return; } /* パスワードのバリデーション */ if (!/^(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[a-zA-Z\d]{8,16}$/.test(password.val())) { password.val(''); message.validation.text('パスワードは半角英数字(大文字/小文字/数字)をそれぞれ1文字以上使用し、8~16文字の範囲で入力してください。').show(FORM_FADE_SPEED); return; } /* フォームが空でないか */ if (userId.val() === '' || email.val() === '' || password.val() === '') { message.validation.text('フォームが空です。').show(FORM_FADE_SPEED); return; } /* 入力内容をシリアライズ */ query = form.serialize(); /* パスワード入力文字をクリア */ password.val(''); /* 誤動作防止のため、送信ボタンを無効化 */ form.find('fieldset').prop('disabled', true); /* 入力内容をAjax送信 */ $.post( './register.php', query, function (json) { var html; if (json && json.message) { /* レスポンスを受け取ったら、フォームの無効化を解除 */ form.find('fieldset').prop('disabled', false); switch (json.message) { /* 登録処理が成功した時 */ case 'success': user.id = json.id; html = $.parseHTML(json.html, false); userId.val(''); email.val(''); /* ユーザー画面に切り替え */ compo.ajaxLogin.empty().append(html); /* マイページへ遷移 */ transit.registerToAdminInit.trigger(); break; /* 不正なユーザー名 */ case 'invalid userId': message.validation.text('ユーザー名は半角英数字かアンダースコア(_)のみ使用できます。4~20文字の範囲で入力してください。').show(FORM_FADE_SPEED); break; /* 不正なメールアドレス */ case 'invalid email': message.validation.text('メールアドレスが正しくありません。').show(FORM_FADE_SPEED); break; /* 不正なパスワード */ case 'invalid password': message.validation.text('パスワードは半角英数字(大文字/小文字/数字)をそれぞれ1文字以上使用し、8~16文字の範囲で入力してください。').show(FORM_FADE_SPEED); break; /* 登録ユーザー名の重複 */ case 'userId duplicated': message.validation.text('登録済みのユーザー名です。').show(FORM_FADE_SPEED); break; /* 登録メールアドレスの重複 */ case 'email duplicated': message.validation.text('登録済みのメールアドレスです。').show(FORM_FADE_SPEED); break; /* 入力時間がタイムオーバー */ case 'expired': message.validation.text('登録期限が過ぎています。リロードを行ってください。').show(FORM_FADE_SPEED); break; /* 不正なアクセス、その他のエラー */ case 'illicit access': case 'not XHR': case 'db error': message.validation.text('不明なエラーが発生しました。リロードを行ってください。').show(FORM_FADE_SPEED); break; } } else { message.validation.text('ユーザー登録に失敗しました。リロードを行ってください。').show(FORM_FADE_SPEED); } }, 'json' ); }); }, exitAction: function () { /* 「ホーム」ボタンと登録画面を隠す */ button.returnHome.off('click').hide(); compo.register.find('button').off('click').end().hide(); /* バリデーション用テキストを空に */ message.validation.text('').hide(); } }); fsm.ajaxLogin.addState(state.register); transit.authToRegister = new Transition('auth-to-register', 'auth', 'register'); transit.registerToAuth = new Transition('register-to-auth', 'register', 'auth'); transit.registerToAdminInit = new Transition('register-to-admin-init', 'register', 'admin-init'); fsm.ajaxLogin.addTransition(transit.authToRegister); fsm.ajaxLogin.addTransition(transit.registerToAuth); fsm.ajaxLogin.addTransition(transit.registerToAdminInit); /* ログイン画面の設定 */ state.login = new State('login', { entryAction: function () { /* ログイン画面を表示 */ compo.login.show(); /* 最初の画面へ戻るボタンを表示 */ button.returnHome.show() .on('click', function (e) { /* ログイン画面から最初の画面へ戻る */ transit.loginToAuth.trigger(); }); /* 「サインイン」ボタンを押した際の処理 */ compo.login.find('button').on('click', function (e) { var form, email, password, query, sha, hash; e.preventDefault(); e.stopPropagation(); $(this).blur(); form = compo.login.find('form'); email = form.find('.email'); password = form.find('.password'); message.validation.text('').hide(); /* 入力値をトリミングする */ email.val($.trim(email.val())); password.val($.trim(password.val())); /* メールアドレスのバリデーション */ if (!/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/i.test($.trim(email.val()))) { email.val(''); message.validation.text('メールアドレスが正しくありません。').show(FORM_FADE_SPEED); return; } /* パスワードのバリデーション */ if (!/^(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[a-zA-Z\d]{8,16}$/.test(password.val())) { password.val(''); message.validation.text('パスワードは半角英数字(大文字/小文字/数字)をそれぞれ1文字以上使用し、8~16文字の範囲で入力してください。').show(FORM_FADE_SPEED); return; } /* フォームが空でないか */ if (email.val() === '' || password.val() === '') { message.validation.text('フォームが空です。').show(FORM_FADE_SPEED); return; } query = form.serialize(); password.val(''); form.find('fieldset').prop('disabled', true); /* ログイン処理をするPHPファイルにXHRリクエスト */ $.post( './login.php', query, function (json) { var html; if (json && json.message) { form.find('fieldset').prop('disabled', false); switch (json.message) { /* 登録処理が成功した時 */ case 'success': user.id = json.id; html = $.parseHTML(json.html, false); email.val(''); /* ユーザー画面に切り替え */ compo.ajaxLogin.empty().append(html); /* マイページへ遷移 */ transit.loginToAdminInit.trigger(); break; /* 不正なメールアドレス */ case 'invalid email': message.validation.text('メールアドレスが正しくありません。').show(FORM_FADE_SPEED); break; /* 不正なパスワード */ case 'invalid password': message.validation.text('パスワードは半角英数字(大文字/小文字/数字)をそれぞれ1文字以上使用し、8~16文字の範囲で入力してください。').show(FORM_FADE_SPEED); break; /* メールアドレスかパスワードが不正 */ case 'failure': message.validation.text('メールアドレスまたはパスワードが正しくありません。').show(FORM_FADE_SPEED); break; /* 入力時間がタイムオーバー */ case 'expired': message.validation.text('ログイン期限が過ぎています。リロードを行ってください。').show(FORM_FADE_SPEED); break; /* 不正なアクセスやその他のエラー */ case 'illicit access': case 'not XHR': case 'db error': message.validation.text('不明なエラーが発生しました。リロードを行ってください。').show(FORM_FADE_SPEED); break } } else { message.validation.text('ログイン認証に失敗しました。リロードを行ってください。').show(FORM_FADE_SPEED); } }, 'json' ); }); }, exitAction: function () { /* 「ホーム」ボタンと登録画面を隠す */ button.returnHome.off('click').hide(); compo.login.find('button').off('click').end().hide(); /* バリデーション用テキストを空に */ message.validation.text('').hide(); } }); fsm.ajaxLogin.addState(state.login); transit.authToLogin = new Transition('auth-to-login', 'auth', 'login'); transit.loginToAuth = new Transition('login-to-auth', 'login', 'auth'); transit.loginToAdminInit = new Transition('login-to-admin-init', 'login', 'admin-init'); fsm.ajaxLogin.addTransition(transit.authToLogin); fsm.ajaxLogin.addTransition(transit.loginToAuth); fsm.ajaxLogin.addTransition(transit.loginToAdminInit); /* 認証後の機能ステート群 */ state.adminInit = new State('admin-init', { autoTransition: true, entryAction: function () { /* ボタンやコンポーネント要素 */ compo.menu = $('#menu'); compo.setting = $('#setting'); compo.logout = $('#logout'); /* バリデーションのテキスト表示用要素 */ message.validation = $('#validation'); /* 最初の画面に戻るためのボタン */ button.returnHome = $('#return-home-button'); button.setting = $('#setting-button'); button.logout = $('#logout-button'); /* いったんコンポーネントを非表示に */ message.validation.hide().removeClass('hidden'); button.returnHome.hide().removeClass('hidden'); compo.menu.hide().removeClass('hidden'); compo.setting.hide().removeClass('hidden'); compo.logout.hide().removeClass('hidden'); } }); fsm.ajaxLogin.addState(state.adminInit); state.menu = new State('menu', { entryAction: function () { compo.menu.show(); compo.menu.find('button') .on('click', function (e) { var target; e.preventDefault(); e.stopPropagation(); target = e.target; if (target.id === 'setting-button') { transit.menuToSetting.trigger(); } else if (target.id === 'logout-button') { transit.menuToLogout.trigger(); } }); }, exitAction: function () { /* 画面遷移の際にイベントリスナーを削除 */ compo.menu.find('button').off('click').end().hide(); } }); fsm.ajaxLogin.addState(state.menu); transit.adminInitToMenu = new Transition('admin-init-to-menu', 'admin-init', 'menu'); fsm.ajaxLogin.addTransition(transit.adminInitToMenu); state.logout = new State('logout', { entryAction: function () { var form, buttons; /* ログアウト画面を表示 */ compo.logout.show(); form = compo.logout.find('form'); buttons = form.find('button'); buttons.on('click', function (e) { var target, query; e.preventDefault(); e.stopPropagation(); buttons.blur(); target = e.target; if (target.id === 'confirm-logout-button') { query = form.serialize(); buttons.addClass('disabled'); $.post( './logout.php', query, function (json) { if (json && json.message) { switch (json.message) { /* ログアウト処理が成功した時 */ case 'success': window.location.href = './'; break; /* 処理が失敗した時 */ case 'illicit access': case 'not XHR': buttons.removeClass('disabled'); message.validation.text('不正なアクセスを検知しました。操作をやり直してください。').show(FORM_FADE_SPEED); break; } } }, 'json' ); } else if (target.id === 'return-menu-from-logout') { transit.logoutToMenu.trigger(); } }); }, exitAction: function () { compo.logout.find('button').off('click').end().hide(); /* バリデーション用テキストを空に */ message.validation.text('').hide(); } }); fsm.ajaxLogin.addState(state.logout); transit.menuToLogout = new Transition('menu-to-logout', 'menu', 'logout'); transit.logoutToMenu = new Transition('logout-to-menu', 'logout', 'menu'); fsm.ajaxLogin.addTransition(transit.menuToLogout); fsm.ajaxLogin.addTransition(transit.logoutToMenu); state.setting = new State('setting', { entryAction: function () { /* 設定画面を表示 */ compo.setting.show(); button.returnHome.show(); }, exitAction: function () { } }); fsm.ajaxLogin.addState(state.setting); transit.menuToSetting = new Transition('menu-to-setting', 'menu', 'setting'); transit.settingToMenu = new Transition('setting-to-menu', 'setting', 'menu'); fsm.ajaxLogin.addTransition(transit.menuToSetting); fsm.ajaxLogin.addTransition(transit.settingToMenu); /* ステートマシンを起動する */ fsm.ajaxLogin.start(); }());
ajax-login.css
@charset "UTF-8"; .container-fluid { padding: 15px; max-width: 768px; max-height: 576px; height: 100vh; min-width: 480px; min-height: 360px; } .container-fluid > #ajax-login { margin-bottom: 0; height: 100%; } #content { height: 100%; position: relative; } #title { margin: auto; color: #333px; font-size: 60px; position: absolute; top: 25%; left: 0; right: 0; text-shadow: 4px 2px 2px #05F; } #return-home-button { position: absolute; right: 10px; top: 10px; } #return-home-button > button { padding-top: 4px; } #return-home-button span { font-size: 18px; color: #777; } #auth { margin: auto; position: absolute; top: 50%; left: 0; right: 0; } #auth > button { margin-right: 10px; } #register { margin: auto; position: absolute; top: 45%; left: 0; right: 0; } #login { margin: auto; position: absolute; top: 45%; left: 0; right: 0; } #validation { margin: auto; padding: 0 10px; width: 360px; position: absolute; top: 15%; left: 0; right: 0; } #validation p { margin-top: 2px; padding: 2px; } #menu { margin: auto; width: 300px; position: absolute; top: 50%; left: 0; right: 0; } #menu > button { width: 100%; white-space: normal; } #setting { margin: auto; width: 65%; position: absolute; top: 50%; left: 0; right: 0; } .delete-border-top { border-top: none !important; } .insert-border-bottom { border-bottom: 1px solid #ddd !important; } #logout { margin: auto; width: 70%; position: absolute; top: 55%; left: 0; right: 0; } #logout form { margin-top: 10px; } #credit { margin: auto; font-size: 12px; position: absolute; left: 0; right: 0; bottom: 0; } #credit address { margin-bottom: 4px; } @media screen and (max-width: 768px) { #title { font-size: 50px; } } @media screen and (max-height: 576px) { #title { top: 15%; } #register { top: 35%; } #login { top: 35%; } #menu { top: 40%; } #setting { top: 35%; } }