もう少しそれらしく
さて、単純にメッセージのやり取りができるようにはなりましたが、もう少しそれらしくなるようにいくつか修正を加えていきます。
永続化先を変える
これまでの実装では、モデルの作成はしていますが、データの永続化先についてはまったく触れていません。Sailsではデフォルトの状態だと、sails-diskというアプリのインスタンスが起動している間のみデータが保持されるモジュールが利用されます。これだとメッセージの履歴はインスタンスが停止されると消えてしまいますし、この後追加する予定のユーザ情報を保持するのにも不都合です。そこで、まずはデータの永続化先を変更することにします。
SailsではWaterlineという独自のデータ永続化エンジンを組込んでおり、アダプターを変更することで永続化先をMySQLのようなRDB、ドキュメント指向のMongoDB、Key-ValueのRedisなどに組み替えて利用できます。ここではMongoDBを採用します。MongoDBと接続するにはsails-mongoというアダプターを利用します。sails-mongoはnpmを使ってインストールしておきます。MongoDBは一応安定版を用意します。筆者の手元環境は2.4.6ですが、執筆現在の最新安定版は2.4.9です。MongoDBの使い方についての説明は割愛しますので、適宜公式サイトなどを参照してください。
sails-mongoのインストールとMongoDBの環境が整ったら、アプリの設定を変更していきます。アダプターの設定はconfig/adapter.jsで行います。追加する記述は以下のようになります。
// If you leave the adapter config unspecified
// in a model definition, 'default' will be used.
//'default': 'disk',
'default': 'mongo',
・
・
・
mongo: {
module: 'sails-mongo',
host: 'localhost',
port: 27017,
database:'chat',
schema: true
}
MongoDBはここでは単体で利用することにしますので、レプリカセット等の設定は追記していません。また、MongoDB側でセキュリティ設定はしていない状態なので、ユーザとパスワードの記述もしていません。必要に応じてsails-mongoのGitHub上のサイトの設定例を参考にして、適宜記述の追加修正を行ってください。MongoDBはスキーマレスモードで利用できますが、ここではグローバルの設定としてスキーマを有効にしています。設定は個別のモデル内でも行うことができます。ひと通りのMongoDB向けの追記を行ったら、デフォルトで利用する永続化先の設定をdiskからMongoDBに変更しておきます。これでMongoDBを永続化先とする準備ができました。
ログインとユーザの追加
永続化先が変更できたら、ログインの機能を追加し、ユーザを識別できるようにします。ログインはUsing OAuth 2.0 for Login(OpenID Connect)を参考にしてOpenID Connectで実装してみようと思います。GoogleのAPIを利用するためには、あらかじめDeveloper Consoleでアプリの登録をしておく必要があります。適宜登録は行っておきます。
Googleでの認証の実装に利用するモジュールは以下のとおりです。
Passport.jsについてはご存知の方も多いかと思います。node.js用の認証ライブラリで、ストラテジを使い分けることでさまざまなサービスでのOpenIDやOAuthを利用した認証・認可の実装が比較的簡単に利用できるようになっています。今回はGoogleのAPIを利用します。本来ならGoogle API用のストラテジ(passport-google-oauth)を使いたいところですが、実装がOpenID Connectに対応していないようなので、OpenID Connect用のストラテジを利用します。
passport-openidconnectは、Passport.jsの作者であるJared Hansonさんご自身が作ったストラテジですが、試験的に作ってみている側面もあるようで、実装時に確認のため入れたと思われるトークンやプロフィールのログがコンソールに出力されてしまいます。少し試すというのではなく、実際に公開するアプリを作成する際には、他のライブラリを使用した方がよいかもしれません。
それではまずUserモデルを作成しておきます。generateはこれまでと同様です。User.jsが生成されたら属性を追加します。ここでは最低限の属性として、IDと名前を持たせます。
/**
* User
*
* @module :: Model
* @description :: A short summary of how this model works and what it represents.
* @docs :: http://sailsjs.org/#!documentation/models
*/
module.exports = {
attributes: {
userId:{type:'string', unique:true},
firstName:'string',
lastName:'string'
}
};
IDはID Tokenに含まれるsubの値をそのまま持たせることにします。今回はGoogleを利用してのログインのみにしていますが、sub値はIDの発行元ごとにユニークなので、例えば複数のOPからのログインを行えるようにする場合は、別途工夫が必要です。
次に、実際にGoogleのAPIを呼び出しユーザ情報の登録とログインを行う実装を追加していきます。認証関連のアクションを持つAuthControllerを生成します。login、logout、authenticate、callbackの各アクションをあらかじめ指定して追加しておきます。callbackはコールバック先としてあらかじめGoogleのDeveloper Consoleで設定したものと同じパスになる必要があります。
/**
* AuthController
*
*/
var passport = require('passport');
module.exports = {
/**
* Action blueprints:
* `/auth/login`
*/
login: function (req, res) {
res.view();
},
/**
* Action blueprints:
* `/auth/logout`
*/
logout: function (req, res) {
req.logout();
res.redirect('/auth/login');
},
/**
* Action blueprints:
* `/auth/authenticate`
*/
authenticate: function (req, res) {
passport.authenticate('openidconnect',{scope:['openid email']})(req,res);
},
/**
* Action blueprints:
* `/auth/callback`
*/
callback: function (req, res) {
passport.authenticate('openidconnect',
{failureRedirect:'login',successRedirect:'/chatRoom/room'})(req,res);
},
loginはログイン画面を単に表示だけするアクションにしてあります。実際に認証の処理を開始するのはauthアクションです。Passportのストラテジ指定としてopenidconnectを指定しています。scopeはopenid emailを指定します。callbackはGoogleでの認証後の戻り先になります。戻ってきた後にチャットアプリ側でのユーザの確認や登録を行うことになります。
ユーザの確認や登録を行う処理はapi/servicesディレクトリ以下に配置します。実装は以下のとおりです。
var passport = require('passport'),
OpenIDConnectStrategy = require('passport-openidconnect').Strategy;
passport.serializeUser(function (user, done) {
done(null, user);
});
passport.deserializeUser(function (user, done) {
User.findOne({userId:user.userId}).done(function(err, user){
if(err){
done(null,null);
} else {
done(null,user);
}
});
});
passport.use(new OpenIDConnectStrategy({
authorizationURL:"https://accounts.google.com/o/oauth2/auth",
tokenURL:"https://accounts.google.com/o/oauth2/token",
userInfoURL:"https://www.googleapis.com/plus/v1/people/me",
clientID:"xxxxxxxxxxxxxxxxxxxxx",
clientSecret: "xxxxxxxxxxxxxxxxxxxxx",
callbackURL: "http://localhost:1337/auth/callback"
},
function(iss, sub, profile, jwtClaims,accessToken,refreshToken,params,done) {
process.nextTick(function(){
User.findOne({userId:sub}).done(function(err, user){
if(err){
return done(null,err);
} else {
if(!user){
User.create({userId:sub, firstName:profile.displayName.givenName,
lastName:profile.displayName.familyName}).done(
function(err,createUser){
if(err){
return done(null,err);
} else {
return done(null,createUser);
}
});
} else {
return done(null,user);
}
}
});
});
}));
ユーザが存在しているかの確認を行って新規のユーザである場合は登録を行っています。もう少し厳密に行う場合はID Tokenの妥当性検証を行ったり、また、自動で登録してしまうのではなく、必要に応じてユーザに登録情報の確認や追加情報を登録してもらう処理を挟むなどしますが、ここでは単に自動で登録してしまっています。また、Passportの処理の流れの中でセッションへのログインユーザ情報のセットなどもこちらで行っています。
ここまでのコードが認証を行うためのおおまかな追加の実装ですが、これに加えてPassportをミドルウェアとして組み込みアプリ起動時に初期化する処理もconfigディレクトリ下にpassport.jsを作成し追加しておきます。
var passport = require('passport')
module.exports = {
express: {
customMiddleware: function(app) {
console.log('Passport Initialize.');
app.use(passport.initialize());
app.use(passport.session());
}
}
};
expressのミドルウェアとして組み込み、初期化を行っています。
あとはビューを追加します。ログインボタンが1つだけある画面を用意し、ボタン押下時にAuthControllerのauthenticateアクションを呼び出すようにしておきます。
// login.ejs <h2 class="form-signin-heading">Sign in</h2> <form class="form-signin" role="form" action="/auth/authenticate" method="get"> <div><input type="submit" value="Sign in"></div> </form>
今のままでは誰が投稿しているのかが分からないので、ログイン後に取得したユーザ情報からユーザ名をメッセージ投稿者として表示し、メッセージ投稿時にはメッセージの横に追加で表示するように修正します。修正を加えるのはChatRoomControllerのroomアクションとroom.ejs、app.js、Messageのモデルクラスです。Messageのモデルクラスには属性として投稿者名を追加します。本来であれば、Messageモデルには名前ではなく投稿者のIDを持たせてアソシエーションでUserモデルから名前を取りたいところですが、執筆時点でのWaterlineでは、まだアソシエーションに対応していません。そのため、あまりカッコはよくありませんが、直接名前をメッセージにもたせています。
