概要
前回の投稿 Node.jsでExpressを使わずシンプルなhttpサービスを作成してみる は、なかなか好評でした。いいね!していただいた方、ありがとうございます。
こんなに簡単にWebサーバーが書けるんだ!という感想は嬉しい限りです。インターネット上のサービスって、たいていテキストベースのやりとりが基本で、わりとシンプルなものなんです。うまく組み合わさっていろんな素敵サービスが生み出されています。
というわけで、今回もわりと基本的な機能である「BASIC認証」を、例によってシンプルに実装して遊び、まなんでいきましょう。
BASIC認証ってなに?
Webページにアクセスする際に、ユーザーIDとパスワードを要求する、わりと良くある感じの認証サービスです。
誰でも見られるpublicサイトに対して、登録された人しか見られないので「memberサイト」なんて呼んだりします。
Wikipedia の Basic認証 の項目は簡潔でよくまとまっていると思います。これをよく読んで、がっつり実装していきましょう。
前回のアプリを拡張します
前回の投稿 Node.jsでExpressを使わずシンプルなhttpサービスを作成してみる で作成したシンプルなWebサーバーを今回も使用します。
初めての方は ダウンロード のセクションからzipファイルを入手してください。
さあ実装してみよう
まずは専用ページを作る
BASIC認証で守るコンテンツをわかりやすく管理するために、公開用の public フォルダの下に、専用フォルダを作成します。今回はベタに member というフォルダ名にしましょう。
そしてそのフォルダに index.html ファイルを新規追加します。中身はとりあえず以下にしておきます。
<!doctype html>
<html lang="ja">
<head>
<title>simple-nodejs member</title>
</head>
<body>
<p>This is <b>Member</b> page.</p>
</body>
</html>
こんな感じで。
アプリを node server.js で起動し、ブラウザで確認してみましょう。まだBASIC認証は実装されていないので、IDなどの入力は求められず、普通に表示されるはずです。
認証エラーを返してみる
さて、BASIC認証を実装するには範囲を示すRealmと、その名称を決める必要があります。それぞれ以下のような変数で定義しておきます。
var basic_realm = "public/member/";
var basic_name = "simple-nodejs_member";
今回の認証の対象は公開フォルダ public の下に作成した members フォルダ以下ですから、basic_realm には "public/member/" をセットしました。
basic_name のほうはユニークであれば問題なさそうなので、今回のアプリ名に、対象フォルダの名前をあわせて設定しました。
さて http.createServer() している Webサーバーのロジックも拡張していきましょう。まずは Realm をチェックする if 文を追加し、従来の処理とは分離します。
var server = http.createServer(function (req, res) {
var url = "public" + (req.url.endsWith("/") ? req.url + "index.html" : req.url);
console.log(url); // for debug
if (url.startsWith(basic_realm)) {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'Basic realm="' + basic_name + '"');
res.end();
}
if (!res.finished) {
// ここに元のWebサーバー処理
}
});
url.startsWith(basic_realm) で認証の対象かどうか判断し、対象であれば認証をおこないます。今の段階では必ず認証は失敗するように実装しています。
ポイントはその後の if (!res.finished) の部分です。res.finished は最初は false で、end() などが実行されて処理が完了すると true になります。つまり認証に失敗していなければ従来のWebサーバーとしての処理を実行することになります。
さてアプリを再起動して、さきほどのアドレスにもう一度アクセスしてみましょう。
今度はBASIC認証のポップアップが表示されたはずです。
サーバー側が401のエラーを返すと、Webブラウザーはヘッダで示されたRealm名をチェックし、既に認証情報を知っていれば、それを使って自動的に再アクセスしてくれます。認証情報を知らなければ、このポップアップを出して認証情報を得ようとします。
ここで何を入力しても、認証は失敗するのでアクセスはできません。むろんこの状態でも、Realm 外のページは問題なくアクセスできます。
開けゴマ!
では、ちゃんと認証してページを開けるようにしましょう。まずは認証をチェックする関数を定義します。
var basic_users = ["QWxhZGRpbjpvcGVuIHNlc2FtZQ=="];
function isUser(_auth) {
for (var l=0; l<basic_users.length; l++) {
if (basic_users[l] == _auth) {
return true;
}
}
return false;
}
basic_users 配列を用意して、登録されたユーザーの認証キーを格納することにします。とりあえず1つ入っていますが、これは資料である Wikipedia の Basic認証 の認証を伴うリクエスト (ユーザ名 "Aladdin"、パスワード "open sesame")をそのまま流用しています。
そしてこの配列をもとに、isUser() 関数でユーザーを認証することができます。まあとりあえず、これでいきましょう!
さてこの追加した関数を使って、認証部分をちゃんと書き直します。以下のような感じ。
if (url.startsWith(basic_realm)) {
var auth = req.headers["authorization"]||"";
if (!auth.startsWith("Basic ") || !isUser(auth.substring(6))) {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'Basic realm="' + basic_name + '"');
res.end();
}
}
まず auth という変数に、ヘッダの Authorization の値をもらってきます。指定されていないと undefined になるので、その後に ||"" を加えることで、かならず文字列が得られるようしています。ま、ここはJavaScriptっぽい書き方ですね。
そしてその後の if 文が認証のメイン部分、今日のハイライト!って感じです。まず auth が Basic で始まることを確認しています。他にもいろいろ認証方法がありますからね。そしてBASIC認証であれば、さきほどの isUser() 関数で登録されたユーザーかどうか確認するわけです。
これらチェックに成功すれば、認証部分は無事クリア、となりますね。良かったです。
でもチェックに失敗すれば、最初に実装した認証失敗、つまり 401 エラーを返す部分が実行されてしまうわけです。
さて、アプリを再起動して認証機能を実際に試してみましょう。適当にIDとパスワードを入力してアクセスできないことを確認した後、ユーザ名 "Aladdin"、パスワード "open sesame"でページが表示されることを確認してください。
よかった、これで member 配下のページも参照できるようになりました。
ユーザーの追加
動作はしましたが、ID が Aladdin で決め打ち、ってのは無理がありますよね。最後にユーザーを追加する手段として、addUser() 関数を用意しましょう。
function addUser(_id, _pw) {
var auth = (new Buffer((_id + ':' + _pw).toString(), 'binary')).toString('base64');
if (!isUser(auth)) {
basic_users.push(auth);
}
}
addUser("yamachan", "123");
最初の auth を生成するところが難しく見えるのですが、これは通常のブラウザであれば
var auth = btoa(_id + ':' + _pw);
で済んでしまうところです。Node.js では何故かBase64変換するbtoa関数がないので、かわりに Buffer を用いて変換するのが一般的なようです。
これでわかるとおり、Basic認証で使われている認証のキーとなる文字列は「ユーザーIDとパスワードをコロン : で繋いで、Base64変換したもの」なんですよね。
そして生成した認証キーをリストに追加すれば、ユーザーの追加は完了です。今回は ID が
yamachan、パスワードは 123 で登録しておきました。皆さんが実装するときには、それぞれ好きな値を設定してみてください。
今回のソースコード
少し長くなってきましたが、一応全部掲載しておきます。
var http = require("http");
var fs = require('fs');
function getType(_url) {
var types = {
".html": "text/html",
".css": "text/css",
".js": "text/javascript",
".png": "image/png",
".gif": "image/gif",
".svg": "svg+xml"
}
for (var key in types) {
if (_url.endsWith(key)) {
return types[key];
}
}
return "text/plain";
}
var basic_realm = "public/member/";
var basic_name = "simple-nodejs_member";
var basic_users = ["QWxhZGRpbjpvcGVuIHNlc2FtZQ=="];
function isUser(_auth) {
for (var l=0; l<basic_users.length; l++) {
if (basic_users[l] == _auth) {
return true;
}
}
return false;
}
function addUser(_id, _pw) {
var auth = (new Buffer((_id + ':' + _pw).toString(), 'binary')).toString('base64');
if (!isUser(auth)) {
basic_users.push(auth);
}
}
addUser("yamachan", "123");
var server = http.createServer(function (req, res) {
var url = "public" + (req.url.endsWith("/") ? req.url + "index.html" : req.url);
console.log(url); // for debug
if (url.startsWith(basic_realm)) {
var auth = req.headers["authorization"]||"";
if (!auth.startsWith("Basic ") || !isUser(auth.substring(6))) {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'Basic realm="' + basic_name + '"');
res.end();
}
}
if (!res.finished) {
if (fs.existsSync(url)) {
fs.readFile(url, (err, data) => {
if (!err) {
res.writeHead(200, {"Content-Type": getType(url)});
res.end(data);
} else {
res.statusCode = 500;
res.end();
}
});
} else {
res.statusCode = 404;
res.end();
}
}
});
var port = process.env.PORT || 3000;
server.listen(port, function() {
console.log("To view your app, open this link in your browser: http://localhost:" + port);
});
ダウンロード
今回のプロジェクトの全6ファイルを zipにまとめました ので、必要でしたらダウンロードしてお使いください!
Bluemix環境にpushしてみる (おまけ)
今回も例によって cf push で Bluemix 環境に上げました。登録したパスワードでアクセスできることが確認できました。
このあたりの詳細は前々回の投稿、IBM Bluemixで最もシンプルなNode.js環境を作成してみるを参考にしてみてください。
この記事を公開直後にはまだサービスが動いているとおもいますので、興味のある方はアクセスしてみてください(笑)
ところで今まで気がつかなかったのですが、Bluemix上では普通にhttpsアクセス(SSL通信)になっていますね!httpモジュールで作っただけなのに。ちょっと嬉しい驚きです。
ライセンス
この投稿に含まれる私の作成した全てのコードは Creative Commons Zero ライセンスとします。自由にお使いください。
Enjoy!
以上、シンプルで、でも自分で使うには十分な機能をもつBASIC認証を実装してみました。いろいろ拡張して遊んでみると、既存のライブラリが多機能で使い易いことにあらためて気がつきます。自分で苦労してわかる、先達たちの居る有り難さ!なんてね。
ではまた!