[前回] Web3.0検証(19)-MeteorでTODO管理アプリの開発(データアクセス制御)
はじめに
前回に続き、セキュリティ関連のデータ保護機能です。
publish/subscribe機能を用いて、データ同期を制御することで、
サーバー側の機微な情報が、誤ってクライアントに送信されないように保護します。
理論: publish(公開)/subscribe(購読)によるデータ同期
[クライアント(subscribe)] <--- (データ) ---> [(publish)サーバー]
- Meteorでは,サーバー上のデータはクライアントにキャッシュされ,リアルタイムに同期される
- 同期は自動的に、双方向で行われる
- 開発者が明示的に同期処理を記述する必要はない
-
Meteor.publish()
とMeteor.subscribe()
を使用し,データ同期の範囲を指定するのみ
-
準備
-
端末を二つ用意
- 端末1: meteor起動/確認
- 端末2: コード修正
-
端末1から、Meteorアプリを実行
$ cd simple-todos-react
$ meteor run
- ブラウザでアプリを確認
-
http://localhost:3000/
にアクセス - デベロッパーツールを開く(F12)
-
デバイスのツールバーを切り替え
アイコンをクリック - デバイスを選択
- 今回は、
iPhone SE
を選択
- 今回は、
-
publish/subscribeの実装
- 今まで、クライアントにデータベースのデータが自動同期されていた
-
Tasks.find()
を呼び出すと、コレクション内のすべてのタスクが取得される - クライアントに個人情報など保存すると、プライバシーが侵害される恐れがある
-
- クライアント側に送信するデータを制御する必要あり
autopublish
パッケージを削除
-
autopublish
パッケージにより、データベースコンテンツがクライアントに自動同期される - 端末2から、安全でないパッケージを削除
$ cd simple-todos-react
$ meteor remove autopublish
- アプリが更新されると、タスクリストは空になる
-
autopublish
パッケージがないので、サーバーからクライアントへの送信データを明示的に指定する必要があるため
-
- データ同期に使用されるMeteor関数
-
Meteor.publish
: サーバーからクライアントにデータを公開する -
Meteor.subscribe
: クライアントで、サーバーから公開されたデータを購読(同期して取り込む)
-
タスクの公開(publish)
- 最初にパブリケーションをサーバーに追加
- 認証されたユーザーからのすべてのタスクを公開
-
Methods
と同様、パブリケーション関数でthis.userId
を使用し、認証されたuserId
を取得可能
-
api
フォルダーにtasksPublications.js
ファイルを新規作成
imports/api/tasksPublications.js
import { Meteor } from 'meteor/meteor';
import { TasksCollection } from '/imports/db/TasksCollection';
Meteor.publish('tasks', function publishTasks() {
return TasksCollection.find({ userId: this.userId });
});
- この関数内で
this
を使用しているため、アロー関数(arrow function)=>
は使用できない- アロー関数は
this
コンテキストを提供しないため - 代わりに、
function
キーワードを用いて、従来の関数定義を使用
- アロー関数は
- サーバーがパブリケーションを登録していることを確認
-
server/main.js
で、tasksPublications
をインポートし、強制的に評価
-
server/main.js
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { TasksCollection } from '/imports/db/TasksCollection';
import '/imports/api/tasksMethods';
import '/imports/api/tasksPublications';
タスクの購読(subscribe)
- クライアントで上述パブリケーションをサブスクライブ
- パブリケーションから変更を受け取るため、
useTracker
フック内でsubscribe
を呼び出す
- パブリケーションから変更を受け取るため、
- 単一の
useTracker
を使用し、TasksCollection
からデータ取得するように、ついでにコードをリファクタリング
imports/ui/App.jsx
... ...
const { tasks, pendingTasksCount, isLoading } = useTracker(() => {
const noDataAvailable = { tasks: [], pendingTasksCount: 0 };
if (!Meteor.user()) {
return noDataAvailable;
}
const handler = Meteor.subscribe('tasks');
if (!handler.ready()) {
return { ...noDataAvailable, isLoading: true };
}
const tasks = TasksCollection.find(
hideCompleted ? pendingOnlyFilter : userFilter,
{
sort: { createdAt: -1 },
}
).fetch();
const pendingTasksCount = TasksCollection.find(pendingOnlyFilter).count();
return { tasks, pendingTasksCount };
});
... ...
<div className="main">
{user ? (
<Fragment>
<div className="user" onClick={logout}>
{user.username || user.services ? user.services.github.username : ''} 🚪
</div>
<TaskForm />
読み込み中ステータス表示
- サブスクリプションデータが準備できていない場合、ユーザーに知らせる必要あり
- アプリのローディングステータスを画面表示
- サブスクリプションの準備ができているかの確認方法
-
subscribe
呼び出しの結果を確認- リターン値は、サブスクリプション状態オブジェクトで、
boolean
を返すready
関数が含まれる
- リターン値は、サブスクリプション状態オブジェクトで、
-
imports/ui/App.jsx
... ...
<div className="filter">
<button onClick={() => setHideCompleted(!hideCompleted)}>
{hideCompleted ? 'Show All' : 'Hide Completed'}
</button>
</div>
{isLoading && <div className="loading">loading...</div>}
<ul className="tasks">
... ...
- 読み込み中表示のスタイル設定
client/main.css
.loading {
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
align-items: center;
font-weight: bold;
}
- 上記修正が反映されると、すべてのタスクが再表示される
- サーバーで、
Meteor.publish
を呼び出すと-
tasks
という名前のパブリケーションが登録される
-
- クライアントで、
Meteor.subscribe
をパブリケーション名で呼び出すと- クライアントはパブリケーションからの下記データをサブスクライブ
- 認証されたユーザーが所有するデータベース内の全タスク
- クライアントはパブリケーションからの下記データをサブスクライブ
- サーバーで、
ユーザー権限を確認
- 現状、誰でもブラウザ
console
を使用し、MeteorMethods
を呼び出せる- 例えば、
remove
メソッドからvalidation
を削除し、有効なtask_id
を渡すと、削除できてしまう - 試しに、DevToolsコンソールタブを使用し、以下のようなコマンドを入力し、Enter
Meteor.call('tasks.remove'、'xtPTsNECC3KPuMnDu');
- 例えば、
- タスク所有者のみ、特定タスクを変更可能に制限する必要あり
- メソッドを変更し、認証されたユーザーがタスク作成ユーザーと一致するか確認
imports/api/tasksMethods.js
... ...
'tasks.remove'(taskId) {
check(taskId, String);
if (!this.userId) {
throw new Meteor.Error('Not authorized.');
}
const task = TasksCollection.findOne({ _id: taskId, userId: this.userId });
if (!task) {
throw new Meteor.Error('Access denied.');
}
TasksCollection.remove(taskId);
},
'tasks.setIsChecked'(taskId, isChecked) {
check(taskId, String);
check(isChecked, Boolean);
if (!this.userId) {
throw new Meteor.Error('Not authorized.');
}
const task = TasksCollection.findOne({ _id: taskId, userId: this.userId });
if (!task) {
throw new Meteor.Error('Access denied.');
}
TasksCollection.update(taskId, {
$set: {
isChecked,
},
});
},
... ...
再度、ブラウザからアプリを確認
- ログイン
- ユーザー名: meteorite
- パスワード: password
ログインユーザーのタスク一覧が正常に表示されました。
おわりに
publish/subscribeを用いて、クライアント/サーバー間のデータやり取りを制御しました。
次回も続きます。お楽しみに。