Netflix Falcorについて(1)
Netflix Falcorについて(2)
Netflix Falcorについて(3)
Netflix Falcorについて(4)
Netflix Falcorについて(5)
今回は、前回のプログラムに認証の枠組みを加えます。login認証(ユーザ名・パスワード)を行い、JWT tokenを生成し、addの実行ロジックにtoken認証を加えます。但し注目点はあくまでもFalcorですので、パスワード認証やJWTの扱いはダミーとします。
最初にクライアント側プログラムを見ましょう。
<html>
<head>
<!-- Do _not_ rely on this URL in production. Use only during development. -->
<script src="//netflix.github.io/falcor/build/falcor.browser.js"></script>
<script>
var model = new falcor.Model({source: new falcor.HttpDataSource('/model.json') });
async function fetch() {
//---------------login
var credentials={ 'username': 'admin', 'password': 'dummy_pass' };
var loginResult = await model.call ( ['login'], [credentials]).then((result) => {
return(result);
});
var tokenRes = await model.getValue('login.token'); // 結果はVirtual JSONの値からget
localStorage.setItem("token", tokenRes); // HTML5のlocalStorageにtokenを保存。クッキーは使わない
//---------------HttpDataSourceにおいてtokenをglobal HTTP requestsに追加する
var headers;
if(localStorage.token) {
headers = {
headers: {
'token': localStorage.token
}
};
}
// HttpDataSourceでは第2引数に、global HTTP requestsへの追加オプションを設定できます
// Expressサーバの req.headers.token にtokenが渡されます
model = new falcor.Model({source: new falcor.HttpDataSource('/model.json', headers) });
//---------------add articles
// ①add前のlengthを取得
await model.
get("articles.length").
then(function(response) {
var res = JSON.stringify(response,null,2);
console.log(res);
document.write("<div>addの前"+res+"</div>");
});
// ②新articleのadd、新_id(newArticleID)をgetする
var newArticle = {
title: "テスト1",
content: "これはテスト1のコンテンツです"
};
var newArticleID = await model.
call( 'articles.add',[newArticle] ).then((result) => {
var res2 = JSON.stringify(result,null,2);
console.log(res2);
document.write("<div>"+res2+"</div>");
return model.getValue(
['articles', 'newArticleID']
).then((articleID) => {
return articleID;
});
});
document.write("<div>"+newArticleID+"</div>");
// ③addした_idをもとに新articleの実体をget
await model.
get(['articlesById',[newArticleID],['_id','title', 'content']]).
then(function(articlesResponse) {
var res2 = JSON.stringify(articlesResponse,null,2);
console.log(res2);
document.write("<div>"+res2+"</div>");
});
// ④add後のlengthを取得
await model.
get("articles.length").
then(function(response) {
var res = JSON.stringify(response,null,2);
console.log(res);
document.write("<div>addの後"+res+"</div><br/>");
});
}
fetch();
</script>
</head>
<body>
</body>
</html>
まず最初にlogin pathを追加し、call()でサーバ側に認証依頼をリクエストします。サーバはJSON Graphの'login.token'パスにtoken値を返しますので、getValue()で値を取得します。取得したtoken値はHTML5のlocalStorageに保存しておきます。
シナリオ的には、次のリクエストからはtokenを付けて送信し、add/delete/updateなどのアクセス権限が必要な時はtokenを見て判断することとします。クライアント側ではHttpDataSource()の第2引数にheaderオプションとしてtokenを指定してmodelを作成します。それ以降のmodel.call()では、自動的にtokenが付いた形でリクエストが送られることになります。それ以外のクライアントの処理は前回のものと同じです。
次にサーバ側を見ましょう。
var falcorExpress = require('falcor-express');
var Router = require('falcor-router');
var jsonGraph = require('falcor-json-graph');
var $ref = jsonGraph.ref;
var $error = jsonGraph.error;
var express = require('express');
var app = express();
var bodyParser = require('body-parser'); // call(POST)メソッド処理のために必要
app.use(bodyParser.urlencoded({extended: false}));
//---------- 以下はMongoDB & Mongooseのお決まりの設定です
var mongoose = require('mongoose');
var databaseUri = 'mongodb://localhost/qiita5db';
mongoose.Promise = global.Promise;
mongoose.connect(databaseUri, { useMongoClient: true })
const articleSchema = {
title:String,
content:String
};
const Article = mongoose.model('Article', articleSchema, 'articles');
//----------
app.use('/model.json', falcorExpress.dataSourceRoute(function (req, res) {
// クライアントから送信されたJWT tokenを取得
var currentToken = req.headers.token;
console.log("### JWT token = "+currentToken); // ★①
return new Router([
//================== call(['login'],[credentials])
{
route: ['login'] ,
call: (callPath, args) => {
/* let { username, password } = args[0]; // === credentials
* ここではまじめな認証は行わず、JWTもダミーを返す
*/
return [ { path: ['login', 'token'], value: '0123456789' }, // ダミーtoken
{ path: ['login', 'username'], value: args[0].username },
{ path: ['login', 'error'], value: false }
];
}
},
//================== get("articles.length")
{
route: 'articles.length',
get: () => { return Article.count({}, (err, count) => count)
.then ((articlesCount) => {
return {
path: ['articles', 'length'],
value: articlesCount
};
})
}
},
//================== call( 'articles.add',[newArticle] )
{
route: 'articles.add',
call: (callPath, args) => { //callの場合はargsにクライアント側の引数[newArticle]が渡される
// JWT tokenを確認してクライアント(現ユーザが)がaddの権利があるかどうかを確認します。
if(currentToken !== '0123456789') {
return { // 今回はerror処理は適当です
path: ['articles'],
value: $error('auth error')
}
}
var newArticleObj = args[0];
var article = new Article(newArticleObj);
return article.save(function (err, data) {
if (err) {
console.info("ERROR", err);
return err;
}
else {
return data;
}
}).then ((data) => {
return Article.count({}, function(err, count) {
}).then((count) => {
return { count, data };
});
}).then ((res) => {
var newArticleDetail = res.data.toObject();
var newArticleID = String(newArticleDetail["_id"]);
var NewArticleRef = $ref(['articlesById', newArticleID]); // 実体へのシンボリックリンク
// addによってJSON Graphは副作用を受けます。resultsによって結果を返すだけでなくキャッシュを更新します
var results = [
{
path: ['articles', res.count-1], // JSON Graphのarticles配列の最後に新article(の$ref)を追加
value: NewArticleRef
},
{
path: ['articles', 'newArticleID'], // addの結果として追加されたarticleの_idを返す
value: newArticleID
},
{
path: ['articles', 'length'], // addの影響を反映させるためcacheのlengthを更新する
value: res.count
}
];
return results;
});
}
},
//================== get(['articlesById',[newArticleID],['_id','title', 'content']])
// articlesByIdがarticle実体の定義になります
{
route: 'articlesById[{keys}]["_id","title","content"]',
get: function(pathSet) {
var articlesIDs = pathSet[1];
return Article.find({ // ハンドラはMongoose Promiseを返す
'_id': { $in: articlesIDs} // DBへの1回のアクセスで必要な全てを配列としてget
}, function(err, articlesDocs) {
return articlesDocs;
}).then ((articlesArray) => {
var results = [];
articlesArray.map((articleObject) => { // getした配列をmapで処理
var articleResObj = articleObject.toObject(); // Mongoose Object を normal Objectに変換
var idString = String(articleResObj['_id']);
results.push({
path: ['articlesById', idString],
value: articleResObj
});
});
return results;
});
}
},
]);
}));
// serve static files from current directory
app.use(express.static(__dirname + '/'));
var server = app.listen(3000);
サーバ側ではパス route: ['login']のcall()処理を追加します。
ここでは送られてきた、ユーザ名/パスワードの認証を行い、tokenを生成してクライアントに送ります。この場合、話を簡単にするために、認証は常に成功するダミーで、tokenもダミーを返しています。returnの具体的な内容に注目してください。
{ path: ['login', 'token'], value: '0123456789' }
ですからクライアントはmodel.getValue('login.token')でtoken値にアクセスできます。
ログインが成功すると、クライアントは次からreq.headers.tokenにtoken値をセットしてリクエストしてきます。サーバ側は以下のコードでそれを取得します。
var currentToken = req.headers.token;
これ以降のアクセス認証がひつようとなる処理に関してはこのtokenをみて yes/no を判断します。
if(currentToken !== '0123456789') {
以下がサーバ側の実行結果です(★①)
[sand@www13134uf falcor-qiita5]$ node index.js
### JWT token = undefined <==ログイン要求時のリクエストにはtokenはない
### JWT token = 0123456789 <==ログイン成功後のリクエストにはtokenが付いてくる
### JWT token = 0123456789
### JWT token = 0123456789
クライアント側のブラウザの実行結果は以下のようになります。
addの前{ "json": { "articles": { "length": 2 } } }
{ "json": { "articles": { "2": [ "articlesById", "59891ac639064a303d34d73c" ], "length": 3, "newArticleID": "59891ac639064a303d34d73c" } } }
59891ac639064a303d34d73c
{ "json": { "articlesById": { "59891ac639064a303d34d73c": { "_id": "59891ac639064a303d34d73c", "title": "テスト1", "content": "これはテスト1のコンテンツです" } } } }
addの後{ "json": { "articles": { "length": 3 } } }
サーバ側の環境作成は以下の通りです
mkdir falcor-qiita5
cd falcor-qiita5
npm init
npm install express --save
npm install falcor-express --save
npm install falcor-router --save
npm install mongoose --save
npm install body-parser --save
npm install falcor-json-graph --save
mongoimport --db qiita5db --collection articles --jsonArray initData.js --host=127.0.0.1
node index.js
初期データは以下の通りです。
[
{
title: '夏休み1日目',
content: '1日目は東武動物公園のプールに行った'
},
{
title: '夏休み2日目',
content: '2日目は森林公園に行ってセミをとった'
}
]
最後にpackage.jsonを掲載しておきます
{
"name": "falcor-qiita4",
"version": "1.0.0",
"description": "mkdir falcor-qiita3\r cd falcor-qiita3\r npm init",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.17.2",
"express": "^4.15.3",
"falcor-express": "^0.1.4",
"falcor-json-graph": "^1.1.7",
"falcor-router": "^0.8.1",
"mongoose": "^4.11.5"
}
}