RESTのアーキテクチャで「クライアント/サーバはステートレスじゃなきゃダメだよね」っていう思想がありますが、セッションとか使ってサーバ側にアプリの状態持たせてたらステートレスじゃないですよね。
ってことで、digest認証までいかないけど、Cookieに認証状態を持たせてステートレスに認証させてみました。
CookieにはユーザーIDと色々混ぜてハッシュ化したパスワードダイジェストに加え、
認証に必要ないくつかの情報を持たせてみました。
ハッシュ化するときpassword_hash()を使おうと思ったら手元のPHPが5.4.24という・・。
バージョンアップが面倒だったのでcrypt()を使用しました。
stateless_auth_test.php
<?php
/**
* benchmark
*/
$before_time = microtime() * 1000000;
/**
* ハッシュ用のsalt
*/
define('SERVER_SALT', 'serversalt');
define('CLIENT_SALT', 'clientsalt');
define('NONCE_SALT', 'noncesalt');
class StatelessAuth
{
/**
* constants
*/
const EXPIRE = 60;
/**
* instance member
*/
private $cookie;
private $time;
private $auth;
private $authenticated_uid;
private $authenticated_pw;
/**
* __costruct
*/
public function __construct()
{
$this->time = time();
$this->cookie = self::set_safe_cookie();
/**
* サンプルなので通るUIDとPWを定義
*/
$this->authenticated_uid = 1111;
$this->authenticated_pw = 'seEe37dmfTQZc';
}
/**
* set_safe_cookie: $_COOKIEをクラス用に変換してセット
*/
protected static function set_safe_cookie()
{
static $cookie;
if (!is_null($cookie)) {
return;
}
/**
* 一応エスケープしておく
*/
$cookie = array();
foreach ($_COOKIE as $name => $value) {
$safe_name = htmlspecialchars($name);
$safe_value = htmlspecialchars($value);
$cookie[$safe_name] = $safe_value;
}
return $cookie;
}
/**
* check_auth: 認証チェック
* @return boolean
*/
public function check_auth()
{
if (!is_null($this->auth)) {
return $this->auth;
}
$this->auth = false;
$uid = isset($cookie['uid']) ? (int) $cookie['uid'] : -1;
if ($uid !== $this->authenticated_uid) {
return $this->auth;
}
$digest = isset($this->cookie['d']) ? $this->cookie['d'] : '';
$time = isset($this->cookie['t']) ? $this->cookie['t'] : '';
$nonce = isset($this->cookie['n']) ? $this->cookie['n'] : '';
$valid_pw = crypt("{$this->authenticated_pw}:${time}:${nonce}", CLIENT_SALT);
if ($digest === $valid_pw) {
$this->set_auth_cookie();
$this->auth = true;
}
return $this->auth;
}
/**
* login: ログインする
* @params int $post_uid
* @params string $post_pw
*/
public function login($post_uid, $post_pw)
{
$uid = (int) $post_uid;
$pw = crypt($post_pw, SERVER_SALT);
if ($uid === $this->authenticated_uid && $pw === $this->authenticated_pw) {
$this->set_auth_cookie();
$this->set_login_cookie();
/**
* ログイン状態にする
*/
$this->auth = true;
}
}
/**
* set_auth_cookie: 認証に必要なクッキー情報をセットする
*/
private function set_auth_cookie()
{
$nonce = crypt($this->time, NONCE_SALT);
$digest = crypt("{$this->authenticated_pw}:{$this->time}:${nonce}", CLIENT_SALT);
setcookie('t', $this->time);
setcookie('n', $nonce);
setcookie('d', $digest);
$this->cookie['t'] = $this->time;
$this->cookie['n'] = $nonce;
$this->cookie['d'] = $digest;
}
/**
* set_login_cookie: ログインした時 uid をcookieにセットする
*/
private function set_login_cookie()
{
$expire = $this->time + self::EXPIRE;
setcookie('uid', $this->authenticated_uid, $expire);
$this->cookie['uid'] = $this->authenticated_uid;
}
}
$stateless_auth = new StatelessAuth();
if (isset($_POST['uid']) && isset($_POST['pw']) && !$stateless_auth->check_auth()) {
$stateless_auth->login($_POST['uid'], $_POST['pw']);
}
if ($stateless_auth->check_auth()) {
echo '認証済み<br>';
} else { ?>
<form action="test.php" method="post">
<div>uid: <input type="text" name="uid"></div>
<div>pw: <input type="password" name="pw"></div>
<div><input type="submit" value="ログイン"></div>
</form>
<?php }
/**
* benchmark
*/
$after_time = microtime() * 1000000;
echo '処理時間:' . sprintf('0.%06d', $after_time - $before_time);
exit;
こんな感じ。
毎回認証処理をすることになるけど、そこまでコストは無さそうです。
rails向けにステートレスで認証するgemを作ってみようかな。