kuluna.class
Qiita

AngularのRoute Guardsにログイン情報を保持させる

Categories: Angular

SPAでログイン情報を保持するのはいつの日も悩みのタネです。
AngularにはRouting Guardsという仕組みがあり、これを使うとAngular側で見られてはいけないコンポーネントを制御することができます。

この記事を書いた時のバージョン

Route Guardsを使う

Milestone 5: Route guards

やり方は簡単で、CanActivateインターフェースを実装したサービスクラスを作り、どのルーティングでログイン情報を確認するか設定するだけです。

まずサービスクラスを作ります。

import { Injectable }     from '@angular/core';
import { CanActivate }    from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate() {
    return true;
  }
}

これはサンプルそのままですが、この場合は常にログイン状態であることを返します。
もちろん作ったAuthGuardはapp.module.tsに登録しましょう。

あとはルーティングでこのAuthGuardを使います。

const adminRoutes: Routes = [
  { path: 'admin', component: AdminComponent, canActivate: [AuthGuard] }
];

adminページを開くと普通に見ることができます。では上のcanActiveメソッドでfalseを返してみるとどうでしょう。adminが開けなくなります。
これはAngularのRoute Guardsという仕組みです。

ログイン情報を保持する

ところで、canActiveメソッドの戻り値ですが

の3つのうちどれかを選べます。PromiseやObservableもOKということはcanActive内でAPI叩いて確認しても良いように設計されているからでしょう。
しかし最終的には**booleanに落とし込まなければなりません。**つまりログインしているかはわかるが、誰がログインしているかの情報はそぎ落とさないといけないのです。

Angularはコンポーネント指向なので、それぞれのコンポーネントがそれぞれの振る舞いを決めます。サイドバーはログイン情報を見てお気に入りの一覧をリスト表示したり、ログインしていなけばログインボタンを表示したり、adminページには入っちゃいけないという制御はそれぞれが行います。
すべてのコンポーネントがログインAPIを叩きに行けば**ひどいことになります。**かといってcanActiveだけではユーザーの特定は不可能です。

そこでAuthGuardを拡張してログイン情報も保持するようにしてみました。

ログイン情報Observableを作る

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs/Rx';

import { Api, User } from '../models/model';

@Injectable()
export class AuthGuard implements CanActivate {
  private userSubject = new BehaviorSubject<User>(new User());

  constructor(private api: Api, private router: Router) { }

  async canActivate(): Promise<boolean> {
    const login: User = await this.checkAsync();

    // ログインしていない場合rootに飛ばす
    if (login.token === undefined) {
      this.router.navigate(['/']);
      return false;
    }
    return true;
  }

  /**
   * ログイン状態を確認します。
   * ログイン状態ならログイン情報が取得でき、未ログインの場合は空のオブジェクトが渡されます。
   */
  async checkAsync() {
    const login: User = await this.api.check().toPromise().catch(e => new User());
    this.userSubject.next(login);

    return login;
  }

  /**
   * ログイン情報をリアルタイムに通知します。
   * 未ログインであれば空のオブジェクトが、
   * ログイン済みであればログイン情報がストリームに流れます。
   */
  getObservable(): Observable<User> {
    return this.userSubject;
  }
}

追加したのはuserSubjectというSubjectオブジェクトです。Subjectはrxjsのライブラリで、ストリームに値を流す時に使います。
これがcheckAsync()メソッドが呼ばれるたびにストリームにログイン情報が流れていきます。
つまりこのストリームを各コンポーネントが受け取ればログイン情報を共有することができます。

例えばユーザー名を表示するかしないかは

export class AppComponent {
  login: User = new User();

  constructor(public auth: AuthGuard) {
    auth.getObservable().subscribe(login => {
      this.login = login;
    });
  }
}

としておけば、HTML側で

<div *ngIf="login.name">{{login.name}}</div>

とするだけです。
Router Guardによってログイン確認が行われるので各コンポーネントはObservableをsubscribeするだけでOKです。

今回はログインしているかどうかをオブジェクトに値があるかどうかで見ていますが、Observableに流すストリームは自由に作れるのでお好みのやり方でできます。

ほんとはこのへんもAngular側でやってくれる仕組みができると嬉しいのですが。