Dominoアクセスになんちゃって認証を追加する
前回「DominoとNode.jsの同居サーバ構築 - Chiburu Systemsのブログ」でNodeからDominoにRESTを通じてデータベースのタイトルを取得し、Webページに反映させました。このとき、認証は埋め込みのユーザ名、パスワードを使いました。今回は、それをWebユーザが認証するところまで持って行きます。
理想を言えば、Dominoで認証してセッションを回すようにしたいところですが、遺憾ながら今回はセキュリティもへったくれもない、非常になんちゃって感の強い方法にしました。その点はご容赦いただきたく。
前回のプロジェクトから追加したパッケージは以下の通りです。
- connect-flash
- crypto
- express-session
- mongoose
- passport
- passport-local
これらを、「npm install --save ****」としてプロジェクトに追加します。追加後のpackage.jsonは以下の通りです。
{ "name": "domtest", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www" }, "dependencies": { "body-parser": "~1.18.2", "connect-flash": "^0.1.1", "cookie-parser": "~1.4.3", "crypto": "^1.0.1", "debug": "~2.6.9", "express": "~4.15.5", "express-session": "^1.15.6", "mongoose": "^4.12.0", "morgan": "~1.9.0", "node-rest-client": "^3.1.0", "passport": "^0.4.0", "passport-local": "^1.0.0", "pug": "2.0.0-beta11", "serve-favicon": "~2.4.5" } }
MongoDB、Mongoose
mongooseはmongodbをNode.jsで扱うモジュールになります。
mongodbはNoSQLデータベースで、データ形式がJSONに似たBSONというもので、JavaScriptと相性もいいので、Node.jsでもよく使われます。今回、mongodbは認証の保管先として使います。最終的に認証をするのはDominoのユーザ情報ですが、Node.jsとDominoはクライアント-サーバの関係なので、Node.js側にログイン情報を持たなければなりません。そこでmongodbを使用して、ユーザが入力したユーザ名、パスワードをお預かりしなければならないので、少しいびつな形をしています。また、mongodbに預けたパスワードは、頑丈なセキュリティで守るか、復号化可能な仕組みで暗号化をかけないと、だだ漏れる危険があります。今回はそのような措置もとらないので、「なんちゃって認証」としてあらかじめお断りしています。
mongodbのインストールやセットアップは他のサイトに譲り、ここでは、mongodbにアカウントを一つ追加するコンソール操作を示します。
> use domtest switched to db domtest > db.users.find() > db.users.insert({username:'*****',password:'*****'}) WriteResult({ "nInserted" : 1 }) > db.users.find() { "_id" : ObjectId("59d9c17b4697f5ca11fc044a"), "username" : "*****", "password" : "*****" }
最初の「use domtest」は「データベースdomtestをデフォルトにします。」という意味で、SQLのように「CREATE DATABASE」する必要はありません。以降の「db」はこのdomtestを表すようになります。
「db.users」というのは、domtestデータベースのusersテーブルというような意味になりますが、これも「CREATE TABLE」せずに使えます。このあたりはNotesDBの「フォーム設計がなくても文書を追加できる」のと似ています。「findメソッド」は文字通り検索コマンドで、引数の指定がなければすべてのレコードを表示します。例えば「db.users.find({username:'John'})とすれば、usernameが「John」であるユーザを検索して表示します。「insertメソッド」はテーブルにアイテムを追加します。テーブル設計なしにアイテムを追加できるところがNoSQLらしさですね。
express-session
express-sessionはExpressのサブモジュールで、Webサーバのセッション機能を司るミドルウェアです。かつてはExpressに同梱されていたようですが、現在は分離しているので、別途インストール必要があります。
connect-flash
connect-flashはセッション機能に「メッセージフラッシュ」機能を追加するモジュールです。通常ページ感のデータ受け渡しはセッションに名前付きのデータを追加することでできますが、メッセージフラッシュは、ログイン時の成功、失敗のような一時的なデータの出し入れを楽にしてくれます。メッセージを追加するには、
req.flash('error', {message:'ログインに失敗しました。'});
とし、取り出すときは、
req.flash('error') // {message: 'ログインに失敗しました。'} を返す。
となり、フラッシュデータの'error'オブジェクトは呼び出し後に削除されます。
crypto
パスワードのハッシュ化のために使いますが、ハッシュ化してしまったパスワードはREST呼び出し時には使えないので、今回は参考として入れています。
app.js
app.jsの役割はExpress全体の初期設定になります。
var express = require('express'); var path = require('path'); var favicon = require('serve-favicon'); var logger = require('morgan'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); // セッション機能 var session = require('express-session'); // フラッシュメッセージ var flash = require('connect-flash'); // 【カスタム】認証機能の初期化 var auth = require('./auth.js'); var index = require('./routes/index'); var users = require('./routes/users'); // 【カスタム】ログイン用のルーティング var login = require('./routes/login'); var app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'pug'); // uncomment after placing your favicon in /public //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); // セッション機能を追加する。 app.use(session({ secret: "notesdomino" , saveUninitialized: false , resave: false })); // フラッシュ機能を追加する。 app.use(flash()); // パスポートモジュールを追加する。 app.use(auth.passport.initialize()); app.use(auth.passport.session()); app.use('/', index); app.use('/users', users); // ログイン用のルーティング追加する。 app.use('/login', login.router(auth.passport)); // catch 404 and forward to error handler app.use(function(req, res, next) { var err = new Error('Not Found'); err.status = 404; next(err); }); // error handler app.use(function(err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; // render the error page res.status(err.status || 500); res.render('error'); }); module.exports = app;
auth.js
app.js内に追加されているカスタムモジュールは2つです。そのうちの一つがauth.jsで、人気のある認証モジュール「Passport」を使って、認証機能を設定しています。
Passportは認証機能を単純化するだけでなく、その魅力はFacebookやTwitterなど多くの認証サイトのストラテジ(認証方式)を持っていることです。今回は基本的なローカルストラテジ(ユーザ名、パスワード方式)を使います。このユーザ情報の保管、検索先がmongodbになり、mongodbを扱うモジュールがmongooseになります。
var passport = require('passport'); var LocalStrategy = require('passport-local').Strategy; var mongoose = require('mongoose'); // 本来ならパスワードをハッシュ化したいところ。 // var crypto = require('crypto'); // // var secretKey = "NotesDomino"; // var getHash = function(target) // { // var sha = crypto.createHmac("sha256", secretKey); // sha.update(target); // return sha.digest("hex"); // }; // MongoDBに接続。 var db = mongoose.createConnection( "mongodb://localhost/domtest" , function(error, res){} ); // ユーザ情報のスキーマ。 var UserSchema = new mongoose.Schema({ username: {type: String, require: true} , password: {type: String, require: true} }); // ユーザ情報のモデル。 var User = db.model("User", UserSchema); // パスワード検証用メソッド。 User.prototype.validPassword = function(pass) { return this.password === pass; // return this.password === getHash(pass); }; // セッションサポート用の直列化、非直列化。 passport.serializeUser(function(user, done) { done(null, user._id); }); passport.deserializeUser(function(id, done) { User.findById(id, function(err, user) { done(err, user); }); }); // ユーザ検証戦略の設定。 passport.use(new LocalStrategy( function (username, password, done) { User.findOne({ username: username }, function(err, user) { if (err) { return done(err); } if (!user) { return done(null, false, {message: 'ユーザID/パスワードが違います。'}); } if (!user.validPassword(password)) { return done(null, false, {message: 'ユーザID/パスワードが違います。'}); } return done(null, user); }) } )); module.exports = { passport: passport };
routes/login.js
ログイン用のルーティングは、GET用と、POST用に分かれます。GETはログイン画面の表示に、POSTはログインデータの検査に使います。
// ログイン用のルーティング。 var express = require('express'); var router = express.Router(); module.exports = { // Passportオブジェクトを受け取ります。 "router": function(passport) { // GET時はログイン画面の表示。 router.get('/', function(req, res) { res.render('login', { message: req.flash('error') }) }); // POST時はログイン検査。 router.post('/' , passport.authenticate('local' , { successRedirect: '/' , failureRedirect: '/login' , failureFlash: true } ) ); return router; } };
views/login.pug
ログインルーティングにある「res.render('login', ...);」の「login」は、viewsフォルダ内のlogin.pugテンプレートを参照しています。極めて簡単なログイン画面を作ります。
extends layout block content if(message) p= message form(action='/login', method='post') input(type='text', name='username') br input(type='password', name='password') br input(type='submit', value='Login')
messageの部分は、res.renderの第2引数が参照元になります。
views/index.pug
index.pugの方は、ログインへのリンクが追加されただけになります。
extends layout block content h1= title p Welcome to #{title} a(href="/login") Login
routes/index.js
正しくログインした場合、そのログイン情報を持って、DominoにRESTでアクセスし、前回紹介したように目的のデータベースのタイトルを表示するようになります。
var express = require('express'); var router = express.Router(); // RESTクライアントモジュールを使用する。 var Client = require('node-rest-client').Client; /* GET home page. */ router.get('/', function(req, res, next) { // リクエスト中にユーザ情報が含まれているか。 if (req.user) { // 認証オブジェクトを使ってRESTクライアントを作成。 var client = new Client({ user: req.user.username , password: req.user.password }); client.get('http://localhost/api/data', function(data, response) { // レスポンスが配列でない場合はエラー。 if (!(data instanceof Array)) { res.render('index', { title: 'Express(' + data.message + ')' }); console.log(data); } else { var item = data.find(function(elem, index, array) { return elem['@filepath'] === 'domtest.nsf'; }); res.render('index', { title: item['@title'] }); } }) .on('error', function (err) { res.render('index', { title: 'Express(Error)' }); console.log(err); }); } // ユーザ情報がないときの表示。 else res.render('index', { title: 'Domino REST' }); }); module.exports = router;
動作確認
プロジェクトフォルダ上で、「npm start」とすれば、localhost:3000でアクセスできるようになります。データサービスが使える同一マシン上のDominoサーバも立ち上げておいて下さい。
未認証の場合はこのようになります。リンクをクリックして、ログイン画面に遷移します。
例えば、ログインに失敗すると次のようになります。
ログインに成功すれば、トップページのタイトルが、NotesDBのタイトルに変化します。
さいごに
Dominoが持っている認証機能をどのように肩代わりするか、その辺りが課題として残りましたが、パスワードデータの保護方法が確立できれば、DominoのデータをJSONで扱えるようになり、DominoのWebサーバとしての負担を軽減することができます。
ユーザの認証ができるようになったので、次はWebSocketによるリアルタイム通信にチャレンジしてみたいと思います。