LoginSignup
22
21

More than 5 years have passed since last update.

Angular2 で認証が必要なコンポーネントを実装する方法

Posted at

前提

Angular2 ではコンポーネントへのアクセス制御を @CanActivate アノテーションで行います。

some.component.ts
@Component({
  selector: 'some-component',
  template: '<div>...</div>',
})
@CanActivate((next, prev): Promise<boolean> | boolean => {
  return true;
})
export class SomeComponent {
}

@CanActivate へはアクセス可否を boolean または Promise<boolean> で返す関数を渡します。

課題

ところで、認証状況(セッション)は SessionService のようなサービスを使って管理することが多いかと思います。

session.service.ts
export class SessionService {

  isActive(): boolean {
    // 認証状況を判定して真偽値を返す何らかの処理
  }

}

さて、これを @CanActivate から使おうとすると、まぁ、使えないわけです。

some.component.ts
@Component({
  selector: 'some-component',
  template: '<div>...</div>',
})
@CanActivate(() => {
    // クラスの外側なので `this` は参照できないので下記はエラーとなる。
    return this._sessionService.isActive();
})
export class SomeComponent {

  constructor(private _sessionService: SessionService) {
  }

}

さて、困った。

解決編、その前に

例えば SessionServicenew でインスタンス化して使えばいいんじゃないか、と思うかもしれません。しかし、それは次のような理由で避けるべきです。

  1. (その是非は別として)SessionService が状態を持っており、シングルトンであることを期待している場合がある。
  2. SessionService が別のサービス(例えば Http)などに依存している場合がある。

そんなわけで、なんとかして判定ロジックに SessionService のインスタンスを DI してやる、というのが目指すべき方向性です。

Angular2 の DI

Angular2 の DI についての基本的な説明は @laco0416 さんが昨年末のアドベントカレンダーに投稿した Angular2のDIを知る を読んでいただくのが一番かと思います。

実際に Angular2 における DI を担っているのは Injector というクラスです。コンポーネントを生成する場合、必ずこの Injector もセットで生成され、コンポーネントの親子構造と同じ Injector の親子構造を持ちます。注入するサービスを親コンポーネントを辿って見つける仕組みは、この Injector の親子構造によって実現されています。

ということはなんとかして Injector のインスタンスをどこかから持ってくることができれば、目的が達成できそうです。

ComponentRef

ところで bootstrap(App, PROVIDERS) の戻り値は Promise<ComponentRef> です。実はこの ComponentRef は次のような情報を持っています。

プロパティ 内容
location: ElementRef 対象のHTML要素
instance: any コンポーネントのインスタンス
componentType: Type コンポーネントの型(コンストラクタ)
injector: Injector Injector のインスタンス(!)

つまり……

app.ts
let app = bootstrap(App, PROVIDERS);
export const injector: Promise<Injector> = app.then(ref => ref.injector);

とすれば Promise<Injector> が取得できそうです。正解にかなり近づきました気がしますが、上記のコードのままでは最上位層であるはずの app.ts に各コンポーネントが依存してしまいます。

解決編

ここまでのまとめ。

  • @CanActivate からはコンポーネントに注入される各サービスにアクセスできない。
  • Angular2 の DI は Injector が行っている。
  • Injector のインスタンスは ComponentRef のインスタンスから取得できる。
  • ComponentRef のインスタンスは bootstrap 関数の戻り値から取得できる。
  • bootstrap の戻り値をそのまま公開(export)すると依存関係がおかしいことになる。

そこで次のように Deferred を導入することで解決します。

session.service.ts
export class SessionService {
  isActive(): boolean {
    // 認証状況を判定して真偽値を返す何らかの処理
  }
}
app-injector.ts
import {Injector} from "angular2/core";
import {Deferred} from "ts-deferred";

export const deferredInjector = new Deferred<Injector>();
export const AppInjector: Promise<Injector> = deferredInjector.promise;
functions.ts
import {AppInjector} from "./app-injector";
import {SessionService} from "./session.service";

export function sessionIsActive(): Promise<boolean> {
  return AppInjector
    .then(injector => injector.get(SessionService))
    .then(sessionService => sessionService.isActive());
}
some.component.ts
import {Component} from "angular2/core";
import {CanActivate} from "angular2/router";
import {sessionIsActive} from "./functions";
import {SessionService} from "./session.service";

@Component({
  selector: 'some-component',
  template: '<div>...</div>',
})
@CanActivate(sessionIsActive)
export class SomeComponent {
}
app.ts
import {Component, ComponentRef} from "angular2/core";
import {bootstrap} from "angular2/platform/browser";
import {RouteConfig, ROUTER_DIRECTIVES} from "angular2/router";
import {deferredInjector} from "./app-injector";

@Component({
  template: '<router-outlet></router-outlet>',
  directives: [ROUTER_DIRECTIVES],
})
@RouteConfig({
  // ルート定義
})
class App {
}

let componentRef = bootstrap(App, [SessionService]);

componentRef.then(ref => deferredInjector.resolve(ref.injector));

上記例では Deferred の実装として拙作の ts-deferred を利用していますが、別の実装を利用しても問題ありません。

最後に

もっといい解決方法をご存じの方は、ぜひコメントで教えてください!

22
21
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
21