AWS Amplifyのログイン状態に応じてIonic 4の画面を切り替える

2019.02.04

前回まで

Ionic 4のアプリケーションを作成して、AWS Amplifyを使ってバックエンドを構築した。ログイン周りのビューはAWS Amplifyで提供される<amplify-authenticator>コンポーネントを利用して構築した。

Angularのモジュール機能を使って画面ごとにモジュールを分割したときに、複数画面で利用するコンポーネントや共通の状態を管理するためのモジュールとしてCoreModuleとSharedModuleを作った。

今回やること

ログイン機能をもつWebアプリケーションやスマホアプリの場合、未ログイン状態でのみアクセスできる画面と、ログイン状態でのみアクセスできる画面と、どちらの状態でもアクセスできる画面の3種類がある。

ログイン状態の管理にAWS Amplifyを使ったとき、そのログイン状態に応じてIonic 4のアプリのルーティングが自動的に切り替わるようにする。

また、ログインが必須のページに未ログインでアクセスした場合はログインフォームにリダイレクトされる処理も実装する。

ログイン状態を扱うAuthServiceを作成する

AWS Amplifyが提供するAmplifyServiceをDIすることで、Angularのサービスやコンポーネントから利用することができる。

ログイン状態を取得するメソッドは見つからなかったが、未ログイン状態amplifyService.auth().currentSession()を実行するとエラーが発生することがわかった。

それを踏まえて、ログイン状態をPromiseで返すメソッドを含むAuthServiceを定義した。



import { Injectable } from '@angular/core';
import { AmplifyService } from 'aws-amplify-angular';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export enum AuthState {
  SignedOut           = 'signedOut',
  SignedIn            = 'signedIn',
  MfaRequired         = 'mfaRequired',
  NewPasswordRequired = 'newPasswordRequired',
}

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  constructor(
    private amplifyService: AmplifyService,
  ) { }

  /**
   * ログイン中か判定
   * @return true/ログイン中
   */
  async isSignedIn(): Promise<boolean> {
    try {
      await this.amplifyService.auth().currentSession();
    } catch {
      return Promise.resolve(false);
    }
    return Promise.resolve(true);
  }

  /**
   * ログイン状況
   */
  get authState$(): Observable<AuthState> {
    return this.amplifyService.authStateChange$.pipe(
      map(state => <AuthState>state.state),
    );
  }

  /**
   * ログアウト
   */
  signOut() {
    this.amplifyService.auth().signOut();
  }
}

このサービス自体は状態を持たないものの、状態を持つAmplifyServiceと密結合なのでCoreModuleに登録してみた。

ログインしたときにメイン画面に遷移する処理を実装する

ログイン画面であるAuthPageのngOnInitでAuthServiceを監視して、ログイン状態に切り替わったら/に移動するように実装した。



import { Component, OnDestroy, OnInit } from '@angular/core';
import { AuthService, AuthState } from '../core/services/auth.service';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { NavController } from '@ionic/angular';

@Component({
  selector: 'app-auth',
  templateUrl: './auth.page.html',
  styleUrls: ['./auth.page.scss'],
})
export class AuthPage implements OnInit, OnDestroy {

  private onDestroy = new Subject();

  constructor(
    private navCtrl: NavController,
    private authService: AuthService,
  ) { }

  ngOnInit() {
    this.authService.authState$.pipe(
      takeUntil(this.onDestroy),
      filter(state => state === AuthState.SignedIn),
    ).subscribe(() => {
      this.navCtrl.navigateRoot('/');
    });
  }

  ngOnDestroy() {
    this.onDestroy.next();
  }
}

ログアウト時にログイン画面に遷移する処理を実装する

同様に、ログアウトしたときにログイン画面に自動的に移動する処理を書く。



<ion-header>
  <ion-toolbar>
    <ion-title>
      Ionic Amplify
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <h1>home.page.html</h1>
  <ion-button (click)="onSignOutClicked()" expand="block">ログアウト</ion-button>
</ion-content>



import { Component, OnDestroy, OnInit } from '@angular/core';
import { NavController } from '@ionic/angular';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { AuthService, AuthState } from '../core/services/auth.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.page.html',
  styleUrls: ['./home.page.scss'],
})
export class HomePage implements OnInit, OnDestroy {

  private onDestroy = new Subject();

  constructor(
    private navCtrl: NavController,
    private authService: AuthService,
  ) { }

  ngOnInit() {
    this.authService.authState$.pipe(
      takeUntil(this.onDestroy),
      filter(state => state === AuthState.SignedOut),
    ).subscribe(() => {
      this.navCtrl.navigateRoot('/auth');
    });
  }

  ngOnDestroy() {
    this.onDestroy.next();
  }

  onSignOutClicked() {
    this.authService.signOut();
  }
}

画面遷移には@ionic/angularから提供されているNavControllerを使った。navigateRootメソッドを使うことでアニメーションなしで画面遷移を行うようにした。

意図しない画面へのアクセスを防ぐためにAngular Routerのガード機能を使う

Ionic 4では各画面にURLが存在するので、URL直打ちでアクセスされてしまったときに意図しない画面が表示されるのは困る。Angular Routerのガード機能を使ってこれを防ぐ。

未ログイン状態でしかアクセスできないページにはUnauthGuardを、ログイン状態でしかアクセスできないページにはAuthGuardを設定する。



import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';

@Injectable({
  providedIn: 'root'
})
export class UnauthGuard implements CanActivate {

  constructor(
    private router: Router,
    private authService: AuthService,
  ) {}

  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.isSignedIn().then(isSignedIn => {

      if (isSignedIn) {
        this.router.navigate(['/']);
      }

      return !isSignedIn;
    });
  }

}



import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {

  constructor(
    private router: Router,
    private authService: AuthService,
  ) {}

  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.isSignedIn().then(isSignedIn => {

      if (!isSignedIn) {
        this.router.navigate(['/auth']);
      }

      return isSignedIn;
    });
  }

}

これらのガードがルーターから読み込まれるように設定する。



import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AuthGuard } from './core/guards/auth.guard';
import { UnauthGuard } from './core/guards/unauth.guard';

@NgModule({
  imports: [
    RouterModule.forRoot([
      { path: '',     loadChildren: './home/home.module#HomePageModule', pathMatch: 'full', canActivate: [ AuthGuard ] },
      { path: 'auth', loadChildren: './auth/auth.module#AuthPageModule', canActivate: [ UnauthGuard ] },
    ]),
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

これで、未ログイン状態で/にアクセスすると/authへ、ログイン状態で/authにアクセスすると/へリダイレクトされるように実装できた。

ついでにAWS Amplifyで提供されているテーマを適用する

src/global.scssにAWS Amplifyのテーマをインポートする行を追加することで、Ionicのボタン等のコンポーネントのスタイルをAmplify風に変更することができる。



// http://ionicframework.com/docs/theming/
@import '~@ionic/angular/css/core.css';
@import '~@ionic/angular/css/normalize.css';
@import '~@ionic/angular/css/structure.css';
@import '~@ionic/angular/css/typography.css';

@import '~@ionic/angular/css/padding.css';
@import '~@ionic/angular/css/float-elements.css';
@import '~@ionic/angular/css/text-alignment.css';
@import '~@ionic/angular/css/text-transformation.css';
@import '~@ionic/angular/css/flex-utils.css';

@import '~aws-amplify-angular/theme.css'; // この行を追加

AWS Amplifyのログイン状態に応じてIonic 4の画面を切り替える

まとめ

今回はAWS Amplifyのログイン状態に応じて画面を遷移する実装と、意図しない画面へのアクセスを防ぐガードを実装した。また、AWS Amplifyで提供されるテーマを利用して見た目を整えた。

次回はいよいよバックエンドのAppSyncのスキーマを定義して、アプリとしての機能を形作っていきたい。 要件がわかりやすく、基本形がシンプルなTwitterを見本にして、Ionic 4とAppSyncを使ってトレースしていこうと思う。