Home

日記
アソシエイト広告ツール (22:00)Edit

試しに作ってみた。blogmapに登録されているASINからランダムに選択された広告を表示する。

<script type="text/javascript" charset="euc-jp" src="http://1470.net/api/asin_cm.php?mode=js;associate=ishinao-22"></script>

なんて貼ってみると、

なんて感じになる。

オプションとして、

  • mode - mode=jsでJavaScriptを返す。指定しないとHTMLで返すんで、JavaScriptを使いたくない場合はHTMLで取得して、自分でそれをサイトに埋め込むプラグインでも書くといい。
  • associate - AmazonアソシエイトID。省略すると俺のアソシエイトIDが使われるんで、どんどん省略してOK。
  • website - blogmapに登録されているサイトID。blgomapで検索して。サイト情報があるサイトの場合は、そのURLにある数値(うちの場合は71738)を指定する。すると、そのサイトからかつて取得したASINの中からランダムでASINが選択される。websiteを指定しなかった場合は、blogmapのメディアランキングの中からASINが選択される。
  • class - 広告全体を囲むdivに指定するclassを指定する。省略時には標準のstyleが適用される。要は標準のスタイルが気に入らない人が、自分でCSSを定義して使う時用。
  • charset - デフォルトのEUC-JP以外の文字コードで結果を取得したい場合に指定する。charset=utf-8でUTF-8、charset=sjisでSHIFT_JIS。でも通常はscriptタグのcharset指定がしてあればそれだけできちんと認識されるはずなんで、このパラメータをわざわざ指定する必要はないと思う。

たとえば、

<script type="text/javascript" charset="euc-jp" src="http://1470.net/api/asin_cm.php?mode=js;associate=ishinao-22;website=71738;class=amazon_cm"></script>

なんてすると、

なんて感じになる。要はtdiary.ishinao.netで今まで紹介したAmazon商品の中からランダムで商品が表示されるようになる。amazon_cmなんてCSSクラスはここでは定義していないんで、デフォルトの状態で表示されている。

blogmapにサイトを登録する方法

textmaniaのURL検索/登録を使って、自サイトのRSSのURLを検索するのが一番確実。

blogmapがASIN情報を収集するには

更新pingの送信に対応したツールでは、ping送信先として、

http://1470.net/api/ping

を登録しておくことで、blogmapからの巡回が確実に行われるようになる。ただし巡回された場合でも、RSSにAmazon商品情報(ASIN)が含まれていないと、ASIN情報が収集されない。

はてなダイアリーならば、「RSSフィードに全文を掲載する」オプションを有効にしておくと確実。movable typeを使っている場合などは、「movable typeでRSS 1.0にcontent:encodedを含める方法」などを参考に、RSS内にASIN情報が含まれるようにしておくといい。

Published At2005-04-09 00:00Updated At2005-04-09 00:00

日記
おまけ: 認証処理の基本 (15:28)Edit

自分で作るblogツール(PHP編)』のおまけ記事として、『認証処理の基本』を追加しました。

っつーか、本当はちゃらっと「TypeKeyの認証を利用して、hnsのGRP機能をblogツールで実現する」みたいなサンプルを書こうと思ったのに、順番に書いていったら全然そこまでたどりつけねーよ。次に認証結果をCookie/セッションに保存する話を書いてから、ようやくTypeKeyの話かなー。でもTypeKeyはTypeKeyで真面目に説明すると話が長くなりそうだな。

Published At2005-04-11 00:00Updated At2005-04-11 00:00

日記
PHP 4/5両用コンストラクタ (16:57)Edit

class Foo
{
function Foo($a, $b)
{
$this->__construct($a, $b);
}
function __construct($a, $b)
{
//ここに処理を書く
}
}

って書き方って、PHP 4/5で同じように動きつつ、特に副作用ないよね? 今PHP 5が動く環境がないんで試してないんだけど。

Published At2005-04-11 00:00Updated At2005-04-11 00:00

日記
おまけ: 認証処理の基本Edit

本書では認証関連の記述は、「ひとまずBASIC認証を使っておきましょうね」でお茶を濁してしまったが、ここでもうちょっと細かい認証周りの話について説明しておく。

一般的な認証では、秘密のパスワードを入力し、それがサーバーに記録されているパスワードと合致する→認証を通る、ということになる。複数のユーザーを判別する場合は、ユーザーを特定するIDとそれに対応したパスワードの組み合わせで認証することになる(ユーザーが1人しかいないならばパスワードだけで認証できる)。

もっとも単純な認証のサンプルを書くと、以下のようになるだろう。

login.html

<form method="POST" action="login.php">
ID: <input type="text" name="userid" size="15"><br>
パスワード: <input type="password" name="pwd" size="12"><br>
<input type="submit" value="ログイン">
</form>

login.php

<?php
$userid
= isset($_POST['userid']) ? $_POST['userid'] : '';
$pwd = isset($_POST['pwd']) ? $_POST['pwd'] : '';

if (
check_login($userid, $pwd)) {
  echo
'認証OK!';
} else {
  echo
'認証できませんでした';
}

function
check_login($userid, $pwd)
{
  
$accounts = array(
    
'test' => 'testpwd',
    
'test2' => 'test2pwd',
  );
  if (!isset(
$accounts[$userid])) {return FALSE;} //そのIDは登録されていません
  
if ($accounts[$userid] != $pwd) {return FALSE;} //パスワードが間違っています
  
return TRUE;
}
?>

login.htmlでユーザーIDとパスワードを入力し「ログイン」ボタンを押すと、認証を通った場合は「認証OK!」と表示され、何らかの理由で認証を通らなかった場合は「認証できませんでした」と表示される。

ここでまず注意する点としては、IDやパスワードを入力するフォームでの投稿には、必ずPOSTメソッドを利用することだ。もしもGETメソッドを利用した場合、IDやパスワードがURLのQUERY_STRINGとして現れてしまい、HTTP_REFERERなどを通して第三者に漏れてしまう危険性が増す。

また、パスワード入力欄には<input type="password">を使用することで、ブラウザ上でその入力内容が平文では見えないようになることも気をつけておこう。これは、横で見ている第三者等にパスワード文字列が漏れないようにするために重要だ。<input type="text">にしておいたのでは、入力されている文字列がそのままブラウザ上で表示されてしまう。

※ちなみにここではSSLの利用は考慮しない。もちろん本当にインターネット上での認証情報の安全なやりとりを考えるならばSSLは必須となるが、個人レベルでのblogツールの認証にSSLを利用するのは希だろう。逆に言うとSSLを利用しないWebアプリケーションでは、それ相応の情報しか取り扱わないようにするべきだ。

ただし、本当は上記のようなサンプルには大きな問題がある。それはソースコード上に平文でパスワード情報が記録されていることだ。ソースコードは(特に共用サーバーでは)第三者によって見られる可能性が高い。また、そうでなくてもパスワードが平文で書かれたデータは、できるだけ持たない方がいい。

パスワードを平文で持たずにどうやって認証処理を行うのかというと、よく使われる仕組みとしては、一方向ハッシュ関数を利用する。一方向ハッシュ関数とは、ある文字列(データ)を元に、非常にランダム性の高い結果文字列(値)を導き出す関数だ。一方向ハッシュ関数で導き出された結果から、その元となった文字列は予測することができず、また非常に似通った(ちょっとしか違わない)文字列を元に計算した場合でも、その結果はまったく異なったものとなる、という特徴がある。一方向ハッシュ関数を使った認証処理は、具体的には以下のようなコードとなる。

login.php

<?php
$userid
= isset($_POST['userid']) ? $_POST['userid'] : '';
$pwd = isset($_POST['pwd']) ? $_POST['pwd'] : '';

if (
check_login($userid, $pwd)) {
  echo
'認証OK!';
} else {
  echo
'認証できませんでした';
}

function
check_login($userid, $pwd)
{
  
$accounts = array(
    
'test' => '342df5b036b2f28184536820af6d1caf',
    
'test2' => '145799acd781cf81c354c4398ae5ac53',
  );
  if (!isset(
$accounts[$userid])) {return FALSE;} //そのIDは登録されていません
  
if ($accounts[$userid] != md5($pwd)) {return FALSE;} //パスワードが間違っています
  
return TRUE;
}
?>

$accountsでは、先ほど平文でパスワードを記録していたところに、なにやら16進数文字列のようなものが記述されている。これはmd5関数を使って、「testpwd」および「test2pwd」という文字列のMD5ハッシュ値を計算した結果だ。一方向ハッシュ関数を使って導き出した結果からは、通常その元となった文字列を得ることが非常に難しい。

つまり、上記のような状態でソースコードに記述しておくぶんには、ソースコードを見られても実際のパスワード文字列がばれることはなく、しかも認証チェック時には、「$accounts[$userid] != md5($pwd)」とすることで確実にそのパスワードが正しいかどうかを確認することができる。

しかし、上記のようなやり方では一つのパスワードから必ず同じMD5ハッシュが生成されてしまうため、もしも同じMD5ハッシュ値が存在した場合、それらは同じパスワードを元にしている(可能性が高い)ことが簡単にばれてしまう。そこで、パスワード認証処理は、crypt関数を使って次のように書いた方がより良い。

login.php

<?php
$userid
= isset($_POST['userid']) ? $_POST['userid'] : '';
$pwd = isset($_POST['pwd']) ? $_POST['pwd'] : '';

if (
check_login($userid, $pwd)) {
  echo
'認証OK!';
} else {
  echo
'認証できませんでした';
}

function
check_login($userid, $pwd)
{
  
$accounts = array(
    
'test' => '$1$vb2.Mc0.$ULZpR42LRQlquCawYCV7A/',
    
'test2' => '$1$3x5.OO4.$VCbPWeaHToMbg6TUwk0sM1',
  );
  if (!isset(
$accounts[$userid])) {return FALSE;} //そのIDは登録されていません
  
if (crypt($pwd, $accounts[$userid]) != $accounts[$userid]) {return FALSE;} //パスワードが間違っています
  
return TRUE;
}
?>

今度は、md5をつかった例では16進数文字列が書かれていたところに、なにやら記号のようなものが書かれている。これは、「crypt('testpwd')」および「crypt('test2pwd')」を実行した結果だ。crypt関数では、第2引数を省略して呼び出した場合、ランダムなsalt値を生成し、それを利用して一方向ハッシュ関数(どのアルゴリズムを使うかは設定による)を実行する。そして結果としてsalt値とハッシュ値を足した結果を出力する。試してみるとわかるが、第2引数を省略して実行したcrypt関数は呼び出すたびに違う結果(違うsalt値を使って計算した結果)を返す。

cryptを使った場合のパスワード認証コードはサンプルのように、cryptの第2引数にsalt値を含む認証コードを渡して実行した結果が、認証コードと合致するかどうかで行う。つまりcrypt関数を使うことで、同じパスワードを元にした場合でも、(salt値が違えば)認証用のコードはまったく別の内容となり、同じパスワードを使っているもの同士でもばれることはなくなる。

上記の説明ではそのロジックがうまく理解できないかもしれないが、このようなコードを使うことで、より安全なパスワード認証が実現できるものだと、パターンとして覚えておけばいいだろう。

『自分で作るblogツール(PHP編)』サポートエントリーに戻る

Published At2005-04-11 15:18Updated At2005-04-11 15:18

日記
おまけ: 認証状態を保存する方法 (15:28)Edit

自分で作るblogツール(PHP編)』のおまけ記事として、『認証状態を保存する方法』を追加しました。

ちょっと書き飛ばしすぎかも。推敲する気力がないんで見直してない。これでようやく次はTypeKeyの話に入れるな。

Published At2005-04-12 00:00Updated At2005-04-12 00:00

日記
おまけ: 認証状態を保存する方法Edit

認証処理の基本」では、どのようにすれば安全に認証処理を実装できるのか説明した。しかし、実際には認証処理を通ったあとに、その状態を持続する必要がある。ここでは認証の結果をどのようにして保存するか説明する。

認証状態を保存する方法としては、Cookieを利用する方法と、セッションを利用する方法の二つが一般的だ。セッションといっても、実際には内部でCookieを使っていたりする(使わない方法もある)のだが、ひとまず細かいことは気にしないでおこう。まずCookieを利用して認証状態を保存する方法を説明する。

Cookieを利用して認証状態を保存する一番シンプルな方法は、以下のようなコードとなる。

<?php

$userid
= isset($_POST['userid']) ? $_POST['userid'] : '';
$pwd = isset($_POST['pwd']) ? $_POST['pwd'] : '';

if (
check_login($userid, $pwd)) {
  
setcookie('userid', $userid);
  echo
'認証OK!';
} else {
  echo
'認証できませんでした';
}

?>

2回目以降のアクセス時

<?php

if (isset($_COOKIE['userid'])) {
  echo
'認証OK!';
} else {
  echo
'認証できませんでした';
}

?>

認証を通らない限りは、CookieにユーザーIDは書き込まれないはずなので、CookieにユーザーIDがある→認証OKというロジックだ。ちなみにCookieの有効範囲(パスやドメイン、有効期間など)は環境に合わせて適切に(必要最低限の狭い範囲で有効になるように)設定して欲しい。

一見このような処理で問題ないように思うかもしれないが、実際にはこのような処理には問題がある。それは本書で紹介した格言「Webブラウザから渡された値は決して信用してはならない」に違反しているからだ。Cookieはブラウザが自由に設定できるパラメータであり、その内容を未検証のままに信用してはいけない。

上記のような仕様で認証を行った場合、ログインIDを知っていれば、誰でもその人の認証状態と同じ状態になることができる。要するに、誰でも簡単に騙りができてしまうのだ。

そこで、今度は簡単に騙りができないようにしてみよう。

フォームからのログイン時の処理

<?php

$userid
= isset($_POST['userid']) ? $_POST['userid'] : '';
$pwd = isset($_POST['pwd']) ? $_POST['pwd'] : '';

if (
check_login($userid, $pwd)) {
  
setcookie('userid', $userid);
  
setcookie('pwd', $pwd);
  echo
'認証OK!';
} else {
  echo
'認証できませんでした';
}

?>

2回目以降のアクセス時

<?php

$userid
= isset($_COOKIE['userid']) ? $_COOKIE['userid'] : '';
$pwd = isset($_COOKIE['pwd']) ? $_COOKIE['pwd'] : '';

if (
check_login($userid, $pwd)) {
  echo
'認証OK!';
} else {
  echo
'認証できませんでした';
}

?>

今度は、フォームからの認証に成功した場合、Cookieにそのとき投稿されたユーザーIDとパスワードを保存する。そして、以降のアクセスではCookieに記録されたユーザーID、パスワードを使って認証を行う。ログインIDは簡単に騙れても、パスワードまで騙ることは難しいので、これならば騙りは防げる。

しかし、このような方法もお薦めしない。というのは、Cookieは秘密の情報を保存する場所としては、非常に信頼おけない場所だからだ。

Cookieはその有効範囲にアクセスする際には、毎回HTTPヘッダとして送信されるため、Cookieにパスワードを持たせた場合、パスワードが何度もインターネット上をそのまま流れることになり、盗聴などで傍受される危険性が増す。また、CookieはJavaScriptからも簡単に読み取ることができるので、クロスサイトスクリプティングなどによってパスワードが盗まれる危険性が高い。

特に後者の危険性に対処するために、第三者がJavaScriptを記述できるようなWebアプリケーション(あるいはドメイン)上では、Cookieに秘密の情報を保存してはいけない。

そこで、たとえば以下のような形で、平文パスワードをCookieに記録せずに認証状態を維持してみよう。

フォームからのログイン時の処理

<?php

$userid
= isset($_POST['userid']) ? $_POST['userid'] : '';
$pwd = isset($_POST['pwd']) ? $_POST['pwd'] : '';

if (
check_login($userid, $pwd)) {
  
setcookie('userid', $userid);
  
$now = time();
  
setcookie('authtime', $now);
  
setcookie('authcode', crypt($userid.$now.'秘密の文字列'));
  echo
'認証OK!';
} else {
  echo
'認証できませんでした';
}

?>

2回目以降のアクセス時

<?php

$userid
= isset($_COOKIE['userid']) ? $_COOKIE['userid'] : '';
$authtime = isset($_COOKIE['authtime']) ? $_COOKIE['authtime'] : '';
$authcode= isset($_COOKIE['authcode']) ? $_COOKIE['authcode'] : '';

if (
crypt($userid.$authtime.'秘密の文字列', $authcode) == $authcode) {
  echo
'認証OK!';
} else {
  echo
'認証できませんでした';
}

?>

このやり方ならば、少なくともパスワードをそのままCookieに入れるよりは安全だ。認証コード生成ロジックがばれても、「秘密の文字列」をある程度の頻度で変えることによって、安全性を高めることができる。また、authtimeをチェックすることで古い認証を強制的に破棄するようなコードも簡単に書ける。

そのほかにも平文パスワードを保存せずに認証状態を維持する方法はいろいろあるので、自分なりの方法を考えてみるといいだろう。

さて、上記のやり方はあくまでもCookie自体に認証情報を含ませていたが、より安全な状態の保存方法としては通常セッション機能が用いられる。PHPではセッション機能が標準で用意されているので、手軽に利用できる。セッションを利用した場合のサンプルは以下のようになる。

<?php

session_start
();

$userid = isset($_POST['userid']) ? $_POST['userid'] : '';
$pwd = isset($_POST['pwd']) ? $_POST['pwd'] : '';

if (
check_login($userid, $pwd)) {
  
$_SESSION['userid'] = $userid;
  echo
'認証OK!';
} else {
  echo
'認証できませんでした';
}

?>

2回目以降のアクセス時

<?php

session_start
();

if (isset(
$_SESSION['userid'])) {
  echo
'認証OK!';
} else {
  echo
'認証できませんでした';
}

?>

Cookieを使ったサンプルの最初の例を、セッションを使うように変更しただけだ。セッションに保存されたデータはCookieと違ってクライアントサイドでは偽造できないので、基本的にそのまま信用して使用することができる。またセッションはCookieのように保存できる容量の制限がないので、ある程度大きなデータも保存しておくことができる。

と、とても便利なセッション機能だが、弱点もある。セッションは通常Cookieに保存されたセッションIDによって、クライアント(ブラウザ)とサーバーを結びつける。セッションIDは非常にランダム性が高く、それ単体では特に意味がないデータだが、それを盗まれることでそのセッションIDに結びつけられた、サーバー上のセッションデータが危険にさらされることになる。

つまりセッションを使った場合は、セッションIDの安全性にすべてがかかっていることになる。セッションにデータを保存する場合には、特にセッションIDが盗まれることがないように気をつける必要がある。セッションIDは設定によっては、QUERY_STRINGに付与する形で使用することもできる。その場合、HTTP_REFERERなどを通してセッションIDが漏洩する危険性があることにも注意しておこう。

さらに、セッションにはもう一つ危険なポイントがある。それは、共用サーバーなどでは、サーバー上に保存されるセッションデータが、他のユーザーによって盗み見られる危険性があることだ。共用サーバーを使う場合は、セッションデータの保存場所(session.save_path)などに気を遣いつつ、この点にも注意しておいた方がいい。

『自分で作るblogツール(PHP編)』サポートエントリーに戻る

Published At2005-04-12 15:19Updated At2005-04-12 15:19

日記
PHP 4にthrow〜catchを載せてみる実験 (10:07)Edit

あらかじめ断っておきますが、単なる実験で実用レベルではないです。

if (!class_exists('exception')) {
function throw($ex)
{
$ex->doCatch();
}
function __catch($ex)
{
die($ex->toString() . "\n" .$ex->getTraceAsString());
}
class Exception
{
var $message = 'Unknown exception';
var $code = 0;
var $file;
var $line;
var $_catchFunction;
var $_trace;
function Exception($catchFunction = '__catch', $message = NULL, $code = 0)
{
$this->_catchFunction = $catchFunction;
$this->message = isset($message) ? $message : $this->message;
$this->code = ($code > 0) ? $code : $this->code;
$this->_trace = debug_backtrace();
$this->file = $this->_trace[0]['file'];
$this->line = $this->_trace[0]['line'];
}
function getMessage()
{
return $this->message;
}
function getCode()
{
return $this->code;
}
function getFile()
{
return $this->file;
}
function getLine()
{
return $this->line;
}
function getTrace()
{
return $this->_trace;
}
function getTraceAsString()
{
return var_export($this->_trace, TRUE);
}
function toString()
{
return "Exception: Code: {$this->code}: {$this->message} in {$this->file} on line {$this->line}";
}
function doCatch()
{
call_user_func($this->_catchFunction, $this);
}
}
}

これで、

throw(new Exception());

とかやると、デフォルトではその時点でのトレースが出力される。

function myCatch($ex)
{
//独自のキャッチ処理
}
throw new Exception('myCatch');

とかやると、独自のキャッチ処理が呼ばれるようになる。

でも強引に指定した関数 or メソッドを呼ばせたところで、関数ならばスコープが別になっちゃうからまっとうな継続処理はできない。メソッドを指定すればインスタンス変数までは一応見えるけど、それでも制約が大きい。

というか、catch処理が終了したあとにthrowの次の行にしか戻れないんで、たいていの場合はcatch処理のあとそのまま終了するしかないのが致命的。結局のところ、trigger_errorを一見例外処理風に書いて見せているだけにすぎない。

exit 2みたいにreturn 2とかやって、関数の呼び出し元の呼び出し元まで脱出できれば、まだ使い出がありそうなんだけど。

というわけで、ただの実験でした。特に実用性はありません。PHP 4でthrow new Exception()と書いてみたかっただけです。

Published At2005-04-13 00:00Updated At2005-04-13 00:00

日記
tDiary移動&noraリビルド (14:35)Edit

tDiaryを旧サーバーに戻したら、トップページは表示されるんだけど、日ごとページを表示するとこける。erbscanでエラーが出ていたんで、erbscanを入れ直してみたけどだめ。うーん。と思いつつ、ひとまずRuby関係のライブラリを順番に入れ直していったら、noraを入れたところで復活した。理屈はよくわからない。Rubyは全然わかっていないんでこういうときに困るんだよなー。

Published At2005-04-13 00:00Updated At2005-04-13 00:00

日記
bk1対応書籍メモ登録bookmarklet (19:48)Edit

bk1っつーか、単にHTML中に最初に見つかった「ISBN:******」みたいな文字列を引っかけているだけだけど。

bookmarklet: bk1用

javascript:if (document.body.innerHTML.match(/ISBN\s*[::]\s*([0-9X\-]+)/i)){var asin=RegExp.$1.replace(/-/g, '');document.location.href='http://1470.net/mm/memo_form.html?asin='+asin;}else{alert('NO ISBN');}

これで、mylistで自分のbk1ブリーダーIDを登録できるようにすれば、bk1で買い物する時用クリッピングツールとして使えるかな。

Published At2005-04-13 00:00Updated At2005-04-13 00:00

日記
Ajax版Trackback Tracer (00:52)Edit

blogmapには実験的にtrackbackの追跡機能を載せていたんだけど、サーバーサイドで複数階層をたどって結果を返していたんじゃ、レスポンスが悪くてやってられないんで、あまり実用的な機能として前面には出していなかった。

で、Ajaxが流行りだしてから、「現在blogmapに組み込んでいるtrackback追跡機能はそのうちAjaxを使って、解析状況に応じてツリーの枝を表示していくように改造しよう」とか言うだけ言って放置していたんだけど、何となく催促された気がしたんで、重い腰を上げてAjax版を作ってみた。

作ってみたらなかなか快適な動作速度。Ajaxはこういう用途では便利だね。たとえば、「 trackback trace: ニートを語る時の立論方法」なんかを試してみるといいかも。

クロスブラウザ対応を真面目にやる気になれないんで、Windows IE/Mozilla系以外で動作するかどうかわからない。と言いながら、Opera 8βで試しに動かしたら動いたよ。あらびっくり。

ちなみに現在見ているエントリーのtrackbackを追跡するbookmarkletは、 「trace!」なんて感じになる。

あるいは自サイトのエントリーに追跡リンクを埋め込みたければ、

http://1470.net/bm/trace.html?url=エントリーのパーマリンク

なんてURLにリンクしておけばいい。パーマリンクはURLエンコードしておくと確実。movable typeだったら個別アーカイブのtrackback関連のあたりにでも、

<a href="http://1470.net/bm/trace.html?url=<$MTEntryPermalink encode_url="1"$>">trace!</a>

とか入れておけばいいのかな? tdiaryはよくわからない。mod_rewriteを入れている場合と入れていない場合、両対応の書き方ってどうするんだろう?

というわけで、blogツール開発者ならびにblogサービス運営者の方々には是非とも「trackback autodiscoveryへの対応」ならびに「trackback ping urlに__mode=rssをつけてRSS形式でデータを出力する機能」に対応して欲しい所存であります。じゃないと自動でtraceできないんで。

Published At2005-04-14 00:00Updated At2005-04-14 00:00