TL;DR
- https://gist.github.com/gin0606/f2433fa1dd1ff9464fe2ac064dddd87b
- 細かい表示の出し分けはこちら↓
やりたいこと
ユーザーの権限やロールなどによる、 react-router で定義した route へのアクセス制御
やらないこと
使用しているライブラリなどの解説
前提
- ES2015
- reactjs v15.x.x
- react-router v2.8.x
- flux
ユーザーの属性はアプリケーションの仕様によるので、今回はユーザーの「強さ」によってアクセスの可不可を決める。
各自、開発するアプリケーションで使用する言葉に置き換えて下さい。
ユーザーの強さ(Strength
)一覧
- yowai: 初級者
- hutuu: 中級者
- tsuyoi: 上級者
- saikyou: 最強
Route 一覧
/
/doujo
/doujo/yowai
/doujo/hutuu
/doujo/tsuyoi
/doujo/saikyou
/doujo/yowai/promotion
/doujo/hutuu/promotion
/doujo/tsuyoi/promotion
各 Route に入れる人
-
/
- 世界
- 大体
App
みたいなのを指定してると思うので、今回もそうします。
-
/doujo
- 道場の入り口
- 誰でも入れる
-
/doujo/:tsuyosa
- 自分が属する強さと、1つ下の道場に入ることが出来る
- 例:中級者なら hutuu と yowai
-
/doujo/:tsuyosa/promotion
- 昇級試験
- 自分が属する強さの試験しか受けることが出来ない
アクセス制御前のコード
hutuu 以上はだいたい同じなので省略👷
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Router, browserHistory } from 'react-router';
class App extends React.Component {
static propTypes = {
children: React.PropTypes.node,
};
render() {
return this.props.children;
}
}
class Doujo extends React.Component {
static propTypes = {
children: React.PropTypes.node,
};
render() {
return (<div>
<h1>道場</h1>
{this.props.children}
</div>);
}
}
class YowaiDoujo extends React.Component {
static propTypes = {
children: React.PropTypes.node,
};
render() {
return (<div>
<h2>初級者向け道場</h2>
{this.props.children}
</div>);
}
}
class YowaiPromotionTest extends React.Component {
static propTypes = {
children: React.PropTypes.node,
};
render() {
return (<div>
<h3>初級者昇級試験</h3>
{this.props.children}
</div>);
}
}
const routes = (
<Route path="/" component={App}>
<Route path="/doujo" component={Doujo}>
<Route path="yowai" component={YowaiDoujo}>
<Route path="promotion" component={YowaiPromotionTest} />
</Route>
</Route>
</Route>
);
const router = (
<Router history={browserHistory}>
{routes}
</Router>
);
ReactDOM.render(
router,
document.getElementById('app')
);
アクセス制御を実装する
ユーザーの強さはサーバーに問い合わせて取得するということにする。
Route の設定
以下のように Route に tsuyosa
を指定していく
const routes = (
<Route path="/" component={App}>
<Route path="/doujo" component={Doujo}>
<Route path="yowai" component={YowaiDoujo} tsuyosa={['yowai', 'hutuu']}>
<Route path="promotion" component={YowaiPromotionText} tsuyosa={['yowai']} />
</Route>
</Route>
</Route>
);
アクセス制御
アクセス制御の実装は Higher Order Component (Inheritance Inversion) パターン(?)を使って実装する。
ReactのHigher Order Components詳解 : 実装の2つのパターンと、親Componentとの比較 | プログラミング | POSTD
HOC については↑これを読んで下さい。
何故 HOC を使うかについては端折るけど、結果として以下2つの良い点があった。
- アプリケーションの大本の Component からアクセス制御の実装を完全に切り離せる
- ルーティングの設定のところに
withAuthorization
とtsuyosa
を書けるので、アクセス制御の設定などを一箇所にまとめられる
HOC の用法として適切なのかについては、各自で判断して下さい。
実装
- Flux の「 Action -> Dispatcher -> Store -> ChangeEvent 」の流れを
setTimeout
で代用する - 起動時に constructor が呼ばれるけど、サーバーとの通信前なので Route の情報を取っておいて後で処理する
startupRoute
- 使い終わったら
null
にする
- routes から設定した
tsuyosa
を取得するのに getTuyosa という関数を実装したけど、実装がデカイので gist に上げた
function withAuthorization(WrappedComponent) {
return class AuthorizationComponent extends WrappedComponent {
constructor(...args) {
super(...args);
this.state = {
// 初期化処理が終わったか判定するために userStrength の初期値は undefined にする
userStrength: undefined,
isAuthorized: true,
// 起動時は userStrength が不明なので、あとで route にアクセス可能か判定する
startupRoute: {
routes: this.props.routes,
location: this.props.location,
},
};
// 今回はサーバー通信出来ないので、 `setTimeout` で代用する
// 実際は Store からの Change Event を受け取って、`setState` を呼ぶ
setTimeout(() => {
this.setState({
userStrength: 'tsuyoi',
});
}, 1000);
}
// <Link> タグでの移動時の制御
componentWillReceiveProps(nextProps) {
if (super.componentWillReceiveProps) { super.componentWillReceiveProps(nextProps); }
const { routes, location } = nextProps;
this.setState({
isAuthorized: this.isAuthorized(routes, location, this.state.userStrength),
});
}
// 起動時の制御
componentWillUpdate(nextProps, nextState) {
if (super.componentWillUpdate) { super.componentWillUpdate(nextProps, nextState); }
if (!nextState.startupRoute) { return; }
if (this.state.userStrength === undefined) { return; }
const { routes, location } = nextState.startupRoute;
nextState.isAuthorized = this.isAuthorized(routes, location, this.state.userStrength);
nextState.startupRoute = null;
}
isAuthorized(routes, location, strength) {
const tsuyosa = getTuyosa(routes, location.pathname);
if (!tsuyosa) { return true; }
return tsuyosa.includes(strength);
}
render() {
if (this.state.userStrength === undefined) { return <h1>準備中</h1>; }
if (!this.state.isAuthorized) { return <h1>君は入れない</h1>; }
return super.render();
}
};
}
Route の設定で使う
以下のように withAuthorization
を使う
const routes = (
<Route path="/" component={withAuthorization(App)}>
<Route path="/doujo" component={Doujo}>
<Route path="yowai" component={YowaiDoujo} tsuyosa={['yowai', 'hutuu']}>
<Route path="promotion" component={YowaiPromotionText} tsuyosa={['yowai']} />
</Route>
</Route>
</Route>
);
これで、 tsuyoi 人が /doujo/yowai/promotion にアクセスしようとしたら「君は入れない」と表示されるようになる。
getTuyosa
は子 Route に tsuyosa
を指定してなかったら、親の tsuyosa
を取りに行くようになってるのだけど、強さとか道場とかでいい例思いつかなくて書いてない。
おしまい。