外部サーバーと通信する際、許可されたクライアントであるかを確認するためにワンタイムパスワードによる認証を必要とする場合があります。
ワンタイムパスワードにはいくつか種類がありますが、ここでは時刻同期型のワンタイムパスワードを使って認証を行ってみます。
この方式は、現在時刻を共通のパスフレーズで暗号化し、クライアントとサーバーで同じ値になるかを確認し、認証します。
注意:ここで提示しているサンプルは解説のために簡易化したものであり、十分な安全性は考慮されていません。
【クライアント側】
<?php
//cURLによるJSONデータ送信
function post_param($url, $params=array()){
$conn = curl_init($url);
$content = json_encode($params);
$header = array(
'Content-type: application/json; charset=UTF-8',
'Content-Length: ' . strlen($content)
);
curl_setopt($conn, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($conn, CURLOPT_POSTFIELDS, $content);
curl_setopt($conn, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($conn, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($conn, CURLOPT_HEADER, $header);
// SSL の証明書を検証しない
curl_setopt($conn, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($conn, CURLOPT_SSL_VERIFYHOST, false);
for ( $retry=0; $retry < 3; $retry++ ) {
$res = curl_exec($conn);
$info = curl_getinfo($conn);
if($res === false || $info === false){
sleep(3);
} else {
break;
}
}
curl_close($conn);
$body = substr ($res, $info["header_size"]);
return json_decode($body, true);
}
date_default_timezone_set('UTC');
// 暗号化用パスフレーズ(必ず変更すること)
$salt = 'bOZGM6IZ5Od05wRMUA3ASUEZsPQOnslf';
// 通信先URL(例)
$url = 'http://localhost/server.php';
// ユーザーID
$id = 1;
$time = strtotime( date("Y-m-d H:i:00") );
$token = hash('sha256', $salt . $id . $time);
$param = array('id' => $id, 'token' => $token);
$res = post_param($url, $param);
echo $res['status'];
・送信内容(例)
Array
(
[id] => 1
[token] => 084750a0b42140de67296799bbfee02f68bebec3a5a5f337e13441367dac8b53
)
【サーバー側】
<?php
// ユーザーによってパスフレーズを切り替える
function get_user_salt($id){
// 本来はMySQLなどのデータベースから取得するべき
switch($id){
case(1): $salt = 'bOZGM6IZ5Od05wRMUA3ASUEZsPQOnslf'; break;
case(2): $salt = '2WsxL8hMfArsYT5psSFdHhR3Gfr9pQfY'; break;
case(3): $salt = 'Si68GYAbnppYUH3745PbzEQQDEYUbiTc'; break;
case(4): $salt = 'tHMKD4F8MHxLEiRbAK8NV3sLbShiJXKs'; break;
case(5): $salt = '9K6nSS9j3GMYk6KDRB3aiYf4k3f24nkd'; break;
default: $salt = ""; break;
}
return $salt;
}
date_default_timezone_set('UTC');
$json = file_get_contents('php://input');
$content = json_decode($json, true);
$id = isset($content['id']) ? $content['id'] : null;
$client_token = isset($content['token']) ? $content['token'] : null;
// データベースからユーザーIDに紐付けられたパスフレーズを取得
$salt = get_user_salt($id);
// 前後1分を調べる
$server_token = array();
for($i=-1;$i<=1;$i++){
$time = mktime( date('H'), date('i') + $i, 0, date('m'), date('d'), date('Y'));
$server_token[] = hash('sha256', $salt . $id . $time);
}
$result = array();
if(in_array($client_token, $server_token)){
$result['status'] = 'SUCCEEDED'; // 認証成功
} else {
$result['status'] = 'FAILED'; // 認証失敗
}
header('Content-type: application/json');
echo json_encode($result);
クライアントの PHP を実行すると cURL でサーバーに ID と認証用トークンが送信され、正しく認証された場合、配列 status として「SUCCEEDED」が返り、失敗した場合「FAILED」が返ります。
パスフレーズを用いてハッシュを行うので、この $salt は推測されにくいランダムな値に変更して下さい。この値はクライアントとサーバーで同じものを使う必要があります。
今回トークンは「パスフレーズ(salt) + ユーザーID + タイムスタンプ」を SHA-256 によるハッシュで暗号化して作りました。
サンプルなのでユーザー別のパスフレーズを取得する際(get_user_salt)、switch() を使って直書きした値を返していますが、
実際にはソースコードに直接書かずに MySQL などのデータベースに登録した内容を返すようにして下さい。
ID が 1 の時の $salt の内容がクライアント側の $salt と同じ値になっています。
時刻を同期するため、サーバーとクライアントのタイムゾーンは共通のものを使用する必要があります。
上の例では UTC(協定世界時) を使っていますが「Asia/Tokyo」などでも問題有りません。
認証には時刻の分単位までを比較するので秒は省きます。
具体的には秒を常に「0 秒」としてタイムスタンプを生成します。
サーバー側では前後一分のずれを許容するために、現在時刻に「-1 分」「+0 分」「+1 分」した3つのタイムスタンプをもとにしたトークン作っておき、クライアントから送信された認証用トークンがその中に含まれているかを in_array() で調べます。
トークンの内容は 1 分ごとに変化するので、パスフレーズ(salt)の内容を知られない限り推測はかなり困難になります。
とはいえ中間者攻撃にはあまり強くないため、より安全な通信を行うのであれば SSL を利用して通信そのものを暗号化して下さい。
送信内容を JSON にしたのは Javascript との連携が取りやすいことと、文字型・数値型の区別があること、多次元の連想配列が使いやすいことが理由です。
PHP で使うことだけを前提としているのであれば serialize() を使うのも良いと思います。