React Routerで認証を制御する方法です。試行錯誤して良さそうな実装方法を発見したのでご紹介します。
アプリに認証があると、画面ごとに、
- 認証済みユーザのみアクセスを許可したいページ
- アカウント設定など
- 認証していないユーザのみアクセスを許可したいページ
- ログイン・新規登録など
- 認証に関係なくアクセスを許可したいページ
- Qiita投稿のようなパブリックなコンテンツなど
の設定が必要になる場合があります。
React Routerのサンプルをいくつか見たところ幾つか認証を制御する方法が示されていました。
-
onEnter
を使う方法
- AuthenticatedComponent
onEnter
を使う方法
本家のサンプルで例示された実装です。現在βのreact-router 1.0.0が必要になります。
function requireAuth(nextState, transition) {
if (!auth.loggedIn())
transition.to('/login', null, { nextPathname: nextState.location.pathname });
}
React.render((
<Router history={history}>
<Route path="/" component={App}>
<Route path="login" component={Login}/>
<Route path="logout" component={Logout}/>
<Route path="about" component={About}/>
<Route path="dashboard" component={Dashboard} onEnter={requireAuth}/>
</Route>
</Router>
), document.getElementById('example'));
僕が試したところ、Flux実装によってはrequireAuth
関数とストアをうまくリンクできないという課題がありました。
AuthenticatedComponent
AuthenticatedComponentは認証制御をするコンポーネントを作り、認証が必要なコンポーネントをwrapする実装パターンです。
import React from 'react';
import LoginStore from '../stores/LoginStore';
export default (ComposedComponent) => {
return class AuthenticatedComponent extends React.Component {
static willTransitionTo(transition) {
if (!LoginStore.isLoggedIn()) {
transition.replaceWith('/login');
}
}
...
render() {
return (
<ComposedComponent {...this.props} />
);
}
}
};
クラスを定義するときに、コンポーネントのクラスをAuthenticatedComponent
で囲むようにします。
import React from 'react';
import AuthenticatedComponent from './AuthenticatedComponent';
export default AuthenticatedComponent(class AccountSetting extends React.Component {
render() {
return <div>...</div>;
}
});
AuthenticatedComponent
はご覧のとおり、認証が必要なコンポーネントすべてに認証コードを記述しないといけないため、新しいページを作った場合、認証コードを記述し忘れるリスクがあります。
このパターンの詳細はAdding authentication to your React Flux appを御覧ください。
認証コンポーネントをルーティングに組み込む方法
以上で見たとおり、onEnter
はFlux実装を選ぶし、AuthenticatedComponenentは記述忘れがありそうでした。大抵のFlux実装とインテグできて、記述忘れも少ない方法が必要ほしくなります。
できれば、ルーティング設定で認証の制御を宣言したいと思いました。認証コンポーネントをルーティングに組み込む方法です。例えば下のようにです。
<Route component={App}>
<Route component={UserOnly}>
<Route path="/" component={Dashboard}/>
<Route path="/settings" component={Settings}/>
</Route>
<Route component={GuestOnly}>
<Route path="/signup" component={SignUp}/>
<Route path="/login" component={Login}/>
</Route>
</Route>
こうしておけば、ルーティング設定を見れば認証制御が一目瞭然で、認証まわりのコードを冗長的に書く必要もありません。
上のコードで示したUserOnly
やGuestOnly
は例によってReact Componentの一種です。UserOnly
の実装サンプルが下のようになります。Flux実装のひとつのReduxを使ったものになっています。@connect
の部分はReduxでストアと繋ぎこむ宣言なので、お使いのFlux実装に読み替えてください。
import React from "react";
import { connect } from "redux/react";
@connect(state => ({
session: state.session
}))
export default class UserOnly {
static contextTypes = {
router: React.PropTypes.object.isRequired
}
componentWillMount() {
this.guestWillTransfer(this.props, this.context.router);
}
componentWillUpdate(nextProps) {
this.guestWillTransfer(nextProps, this.context.router);
}
guestWillTransfer(props, router) {
const { loggedIn } = props.session;
if (loggedIn === false) {
router.replaceWith("/login");
}
}
render() {
return (
<div>{this.props.children}</div>
);
}
}
UserOnlyなどはコンポーネントなのでストアの状態(つまりログイン状態)が変化すると、componentWillMount
やcomponentWillUpdate
が呼ばれます。ここでフックして、認証できなかった場合、リダイレクトしているわけです。
ついでにGuestOnlyの実装です。
import React from "react";
import { connect } from "redux/react";
@connect(state => ({
session: state.session
}))
export default class GuestOnly {
static contextTypes = {
router: React.PropTypes.object.isRequired
}
componentWillMount() {
this.userWillTransfer(this.props, this.context.router);
}
componentWillUpdate(nextProps) {
this.userWillTransfer(nextProps, this.context.router);
}
userWillTransfer(props, router) {
const { loggedIn } = props.session;
if (loggedIn === true) {
router.replaceWith("/");
}
}
render() {
return (
<div>{this.props.children}</div>
);
}
}
以上が、認証コンポーネントをルーティングに組み込んで、認証を制御する方法でした。