L08084のブログ

技術記事の執筆は祈りに似ている

【Angular】Guardで認証されていないアカウントをブロックする

やりたいこと

  • ログイン(認証)を突破していないアカウントが、ログイン・アカウント登録画面以外にアクセスできないようにしたい
    • 現状だとURL(/home)を入力すると認証されていないユーザーでもホーム画面にアクセスできてしまう
  • ログインしている状態のアカウントがログイン・アカウント登録画面にアクセスできないようにしたい

前提

下記の対応についてはすでに完了しているものとします(対応内容の詳細については過去記事をご参照ください)

  • Angular CLIによるAngularプロジェクトの作成
  • メールアドレスとパスワードを用いたFirebase認証
  • ログアウト機能の実装
  • ログイン・ホーム画面などの作成

Guardを作成する

Angular CLIでプロジェクトを作成している場合は、下記のコマンドでauthentication.guard.tsファイルが作成されます。

$ ng g guard authentication

CLIで作成された初期状態のauthentication.guard.tsです。

  • authentication.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthenticationGuard implements CanActivate {
  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return true;
  }
}

canActivateメソッドでは、Guardを設定したURLに対してのアクセスを許可する場合はtrueを、アクセスを許可しない場合はfalseを返すようにします。

ログインしてないアカウントをホーム画面に入れない

空になっているcanActivateメソッドに、ログインしていないアカウントをホーム画面に遷移させない処理を書いていきます。

  • authentication.guard.ts
import { Injectable } from '@angular/core';
import {
  CanActivate,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  UrlTree,
  Router
} from '@angular/router';
import { Observable } from 'rxjs';
import { AngularFireAuth } from '@angular/fire/auth';
import { take, map } from 'rxjs/operators';

/**
 * ログインしていないアカウントをログイン画面に遷移させる
 *
 * @export
 * @class AuthenticationGuard
 * @implements {CanActivate}
 */
@Injectable({
  providedIn: 'root'
})
export class AuthenticationGuard implements CanActivate {
  constructor(private afAuth: AngularFireAuth, private router: Router) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    return this.afAuth.user.pipe(
      take(1),
      map(user => {
        if (user != null) {
          // ログインしていた場合userにユーザーの情報が入る
          return true;
        } else {
          // ログインしていない場合はログイン画面に遷移する
          this.router.navigate(['/login']);
          return false;
        }
      })
    );
  }
}

AngularFireAuth.userObservable<User|null>を返すため、usernullならばログインしていない、userが設定されていればログインしている、といった判定をすることができます。

作成したAuthenticationGuardAppRoutingModuleに設定するとGuardの実装が完了です。

  • app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { LoginComponent } from './login/login.component';
import { SignUpComponent } from './sign-up/sign-up.component';
import { AuthenticationGuard } from './authentication.guard';

const routes: Routes = [
  { path: '', redirectTo: '/login', pathMatch: 'full' },
  { path: 'login', component: LoginComponent },
  {
    path: 'home',
    component: HomeComponent,
    canActivate: [AuthenticationGuard] // <- add this!
  },
  { path: 'sign-up', component: SignUpComponent }
];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

ログイン済みのアカウントをログイン画面に入れない

AuthenticationGuardでやったことの逆をやります。まず、Angular CLIでGuardファイルを作成します。

ng g guard authenticated

作成されたauthenticated.guard.tsファイルを使って、ログイン済みのアカウントがログイン・アカウント登録画面に遷移するのを禁止する処理を実装します。

  • authenticated.guard.ts
import { Injectable } from '@angular/core';
import {
  CanActivate,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  UrlTree,
  Router
} from '@angular/router';
import { Observable } from 'rxjs';
import { AngularFireAuth } from '@angular/fire/auth';
import { take, map } from 'rxjs/operators';

/**
 * ログイン済のアカウントをログイン・アカウント登録画面に、
 * 遷移させない
 *
 * @export
 * @class AuthenticatedGuard
 * @implements {CanActivate}
 */
@Injectable({
  providedIn: 'root'
})
export class AuthenticatedGuard implements CanActivate {
  constructor(private afAuth: AngularFireAuth, private router: Router) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    return this.afAuth.user.pipe(
      take(1),
      map(user => {
        if (user != null) {
          // ログインしていた場合はホーム画面に遷移する
          this.router.navigate(['/home']);
          return false;
        } else {
          // ログインしていない場合は遷移を許可する
          return true;
        }
      })
    );
  }
}

今度はusernullの場合に、Observable型のtrueを返して画面への遷移を許可しています。反面、usernullでないログイン済のアカウントの遷移は許可せずにホーム画面に遷移させています。

作成したAuthenticatedGuardloginsign-upのルーティングに設定すると、認証済のアカウントによるログイン・アカウント登録画面への遷移を禁止することができます。

  • app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { LoginComponent } from './login/login.component';
import { SignUpComponent } from './sign-up/sign-up.component';
import { AuthenticationGuard } from './authentication.guard';
import { AuthenticatedGuard } from './authenticated.guard';

const routes: Routes = [
  { path: '', redirectTo: '/login', pathMatch: 'full' },
  {
    path: 'login',
    component: LoginComponent,
    canActivate: [AuthenticatedGuard] // <- add this!
  },
  {
    path: 'home',
    component: HomeComponent,
    canActivate: [AuthenticationGuard]
  },
  {
    path: 'sign-up',
    component: SignUpComponent,
    canActivate: [AuthenticatedGuard] // <- add this!
  }
];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

動作確認

当初の仕様通りにアクセスをブロックできているか確認します。

未ログインのアカウントをホーム画面に遷移させない

ログインしていない状態から、不正にURLを書き換えて、ホーム画面に遷移しようとします。

f:id:l08084:20191002191401p:plain
URLを`login`から`home`に変える

ホーム画面に遷移せずに、ログイン画面に戻されました。成功です。

f:id:l08084:20191002191609p:plain
ホーム画面に遷移せずにログイン画面に戻る

ログイン済のアカウントをログイン画面に遷移させない

今度はログイン済のアカウントを使って、ホーム画面からログイン画面への遷移を試みます。

f:id:l08084:20191002192137p:plain
URLを`home`から`login`に変える

URLをloginに書き換えて不正アクセスを試みますが、失敗します。こちらも成功です。

f:id:l08084:20191002192432p:plain
ガードされてホーム画面に戻る

バージョン情報

  • Angular v7.2.0
  • firebase: v6.3.4
  • Angular Material v7.3.7

参考サイト

angularfire2/getting-started.md at master · angular/angularfire2 · GitHub