16 August 2008

(PHP)ユーザがコントロール可能なセキュア認証

HTTPSやダイジェスト認証が使えない共用レンタルサーバで、よりセキュアな認証を目指す方法。

Perl版はこちら

ユーザ名は
・サーバ側から送付されたランダム・キーを付加してブラウザのJavaScriptでMD5ハッシュ生成
パスワードは
・クライアント側のブラウザのJavaScriptでMD5ハッシュ生成
サーバ側では
・既存のPEAR Auth用のサーバ側の認証DBが流用可能

という手法で、通信系路上でキャプチャされたとしても、ある程度安全が保てるのではなかろうか

ログオン用のメインスクリプト

index.php

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=shift_jis" />
<meta http-equiv="Content-Language" content="ja" />
<title> </title>

<script type="text/javascript" src="../cgi-bin/utf.js"></script>
<script type="text/javascript" src="../cgi-bin/md5.js"></script>
<script type="text/javascript" src="../cgi-bin/authpage_form_md5.js"></script>

</head>
<body>

<?php

require_once('auth_md5.php');
require_once('auth_db.php');

$strReturn = CheckAuth('index.php');

if($strReturn)
{
print("<p>ユーザ ".$strReturn." としてログオンしています</p>");
print("<a href=\"logoff.php\">ログオフする</a>");
}
else
{
print("<p>ログオンしていません</p>");
print("<a href=\"index.php\">ログオン画面を表示する</a>");
}

?>
</body>
</html>

ログオフ用に、index.phpからリンクが作成されるスクリプト

logoff.php

<html>
<body>
<?php
require_once('auth_md5.php');
LogoffAuth();
?>
<a href="./index.php">再度ログオンする</a>
</body>
</html>

認証のためのインクルードPHPファイル

auth_md5.php

<?php
// 認証メイン関数
//
// 認証されている場合 TRUE を返す
// 認証されていない場合 FALSE を返す
function CheckAuth($strReloadPage)
{
session_start();
$strAuthUser_authmd5 = ''; // 認証されたユーザ名

if (isset($_SESSION['user']))
{
// 既にログオンしてている場合
if($_SESSION['logontime'] + 60 < time())
{
// ログオン後、一定時間以上経っている場合は強制ログオフ
$_SESSION = array(); // セッション変数を全てクリア
session_destroy(); // セッションファイルを削除
return('');
}
else
{
$strAuthUser_authmd5 = $_SESSION['user'];
return($strAuthUser_authmd5);
}
}
elseif(!$_POST['user'] || !$_POST['password'])
{
// クライアントでハッシュを作成する時の、追加文字列(キー)を作成する
$strRandSeed = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
$strKey = '';
for($i=0; $i<16; $i++)
{
$strKey .= $strRandSeed[rand(0,61)];
}
// キーをセッション変数に格納しておく
$_SESSION['key'] = $strKey;

// 新規入力画面を表示する
print("<form method=\"post\" action=\"./$strReloadPage\" name=\"form2\">\n");
print("\t<table border=\"0\" cellpadding=\"2\" cellspacing=\"0\" align=\"center\">\n");
print("\t<tr><td colspan=\"2\"><strong>ログオン送信データ</strong></td></tr>");
print("\t<tr><td>ユーザ</td><td><input name=\"user\" size=\"40\" readonly=\"readonly\" /></td></tr>\n");
print("\t<tr><td>パスワード</td><td><input name=\"password\" size=\"40\" readonly=\"readonly\" /></td></tr>\n");
print("\t<tr><td colspan=\"2\"><input type=\"submit\" value=\"ログオンする\" /></td></tr>\n");
print("\t</table>\n");
print("</form>\n");


print("<form action=\"./$strReloadPage\" name=\"form1\">\n");
print("\t<table border=\"0\" cellpadding=\"2\" cellspacing=\"0\" align=\"center\">\n");
print("\t<tr><td colspan=\"2\"><strong>ユーザ入力欄</strong></td></tr>");
print("\t<tr><td>ユーザ</td><td><input type=\"password\" name=\"user\" size=\"40\" /></td></tr>\n");
print("\t<tr><td>パスワード</td><td><input type=\"password\" name=\"password\" size=\"40\" /></td></tr>\n");
print("\t<tr><td>Key</td><td><input name=\"key\" size=\"40\" value=\"$strKey\" readonly=\"readonly\" /></td></tr>\n");
print("\t<tr><td colspan=\"2\"><input type=\"button\" value=\"MD5変換\" onclick=\"convert_to_md5()\" /><input type=\"reset\" value=\"入力欄消去\" /></td></tr>\n");
print("\t</table>\n");
print("</form>\n");

return($strAuthUser_authmd5);
}
else
{
// DBを参照して、認証チェックを行う
$strAuthUser_authmd5 = QueryMd5User($_POST['user'], $_POST['password'], $_SESSION['key']);
if($strAuthUser_authmd5 != '')
{
// 認証OK
$_SESSION['user'] = $strAuthUser_authmd5;
$_SESSION['logontime'] = time(); // ログオン時刻を保存
return($strAuthUser_authmd5);
}
}
// 認証失敗
return($strAuthUser_authmd5);
}

// 認証状態をログオフする
function LogoffAuth()
{
session_start();
$_SESSION = array(); // セッション変数を全てクリア
session_destroy(); // セッションファイルを削除
}

?>

認証DBに問い合わせるインクルードPHPスクリプト

auth_md5.php

<?php

// 認証に成功したときに、ユーザ名を返す。
// 認証に失敗、DBエラーのときは ''(空の文字列)を返す
function QueryMd5User($strUser, $strPassword, $strKey)
{

$strMySqlSrv = 'localhost:3306';
$strMySqlUser = 'username';
$strMySqlPassword = 'userpassword';
$strMySqlDb = 'auth_db';

// MySQL 接続
if (!($cn = mysql_connect($strMySqlSrv, $strMySqlUser, $strMySqlPassword))) {
return '';
}

// MySQL DB 選択
if (!(mysql_select_db($strMySqlDb))) {
// MySQL 切断
mysql_close($cn);
return '';
}

// SQL クエリ文(全ユーザを読み出す)
$strSql = "select user,password from auth_tbl";

// SQLクエリを実行する
if (!($rs = mysql_query($strSql))) {
// MySQL 切断
mysql_close($cn);
return '';
}

// 読み出されたデータ数が1以上であることをチェック
if(mysql_num_rows($rs) < 1)
{
mysql_close($cn);
return '';
}

while ($item = mysql_fetch_array($rs))
{
// 認証テーブル上の、一つ一つのユーザ・パスワードについて、同一か確認する
if(md5($strKey.$item['user']) == $strUser && $item['password'] == $strPassword)
{
// ユーザ名とパスワードが一致した
// MySQL 切断
mysql_close($cn);
// 認証成功の場合、ユーザ名を返す
return $item['user'];
}
}

// MySQL 切断
mysql_close($cn);

// 認証テーブル上に一致するユーザが存在しなかった(認証失敗)
return '';
}

?>

そして、クライアント側でMD5変換するためのJavaScript関数は、Masanao Izumo氏のページよりコピーして利用させていただいた。

md5.jsutf.jsを用いる 。

authpage_form_md5.js

function convert_to_md5() {
var f1 = document.form1;
var f2 = document.form2;
var data;

// 「キー + ユーザ名」のMD5ハッシュ作成
data = utf16to8(f1.key.value + f1.user.value);
f2.user.value = MD5_hexhash(data);
f1.user.value = "";

// 「パスワード」のMD5ハッシュ作成
data = utf16to8(f1.password.value);
f2.password.value = MD5_hexhash(data);
f1.password.value = "";
}