Angularのガード(Guard)
前回、AngularのSPAのためのひな型を作成しました。
しかしそこにはログインページを設けていませんでした。
今回はSPA(Slingle Page Application)の必須の機能ともいえるログインページを作成するために、ルーターの Guardについて学びます。
Guard(ガード)はその名の通りAngularのルーターの挙動をガードするものです。認証されていないユーザーを拒否するだけでなく、フォーム入力途中に誤って他のページに移動してしまわないようにするような使い方もできます。
Guardの機能によりログインの状態に合わせてページの表示の可否を決定します。
Guardの作成
ルーターが利用できるようになっていれば、Guardも利用できるようになっています。ルーターの設定につきましては過去の記事を参考にしてください。
コマンドラインでsrc¥appに移動してgenerate guard ガード名を入力します。。ここでは(Windows PowerShellを使っています)
するとGuardの種類の入力を求められます。認証に応じた表示可否を設定する場合はCanActivateを選択します。
PS> ng generate guard guard-name ? Which interfaces would you like to implement? (Press <space> to select, <a> to toggle all, <i> to invert s election) >(*) CanActivate ( ) CanActivateChild ( ) CanDeactivate ( ) CanLoad
作成されるファイルは次のようになっています。
guard-name.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthGuardGuard implements CanActivate {
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return true;
}
}
初期状態のcanActivateメソッドではtrueを返しているのでGuardを設定していない時と同様の挙動になります。
戻り値は中の次のどれかの値を返すようになっています。
- Obsarvable<boolean>
- Obsarvable<UrlTree>
- Promise<boolean>
- Promise<UrlTree>
- boolean
- UrlTree
ObservableはAngularで利用されている非同期処理のライブラリRxJSのクラスです。PromiseはJavaScriptの非同期処理用のクラスです。
同期処理または非同期処理(RxJSまたはPromise)で、booleanかUrlTreeを返せばいいということです。
UrlTreeというのは特定の記述形式になっているURLと理解するとわかりやすいと思います。
公式のドキュメントにもあるように、次のようにcanActivateメソッドで単純にboolean値のfalseを返すと移動はキャンセルされます。
guard-name.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class YourGuard implements CanActivate {
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean {
return false;
}
}
Guardをルーターへ設定
これをルーターに設定するには次のようにします。pathというアドレスでSomeCompornentが表示されるようしてある設定に、canActivateを追加することで、そのコンポーネントの表示可否をGuardに委ねます。
app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SomeComponent } from './some/some.component';
import { GuardNameGuard } from './guard-name.guard';
const routes: Routes = [
{ path: 'path', component: SomeComponent, canActivate: [GuardNameGuard] }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
上記の設定では移動がキャンセルされるだけでした。コンポーネントの表示が許されない時はページのルートに移動されます。
通常の認証の場合、こういった時はログインページに移動させたいと思います。そういった場合はUrlTreeに移動させたいアドレスを指定して返します。
UrlTreeインターフェースの仕様に合わせて、戻り値を作成すればいいのですがRouterインスタンスを使うと簡単に作成できます。
先ほどのGuardのスクリプトにそれを追加してみます。まず、import部分とconstructor部分にRouterを追加します。認証が必要な場合にログイン画面にリダイレクトさせ、さらにlogin成功時に戻ってくるページ(コンポーネント)を知らせています。RouterStateSnapshotのurlに本来移動したい場所のアドレスが入っています。
guard-name.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class YourGuard implements CanActivate {
constructor(private router: Router) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean | UrlTree {
//return false;
return this.router.parseUrl('login-page?url='+state.url);
}
}
CanActivateインターフェースの実装
ログイン状態とGuardの同期の取り方ですが、ここではLocalStorageに利用期限を保存することにします。tokenという変数名に利用期限セットするようにし、Guardではその変数の有無や利用期限を値チェックして問題なければtrueを返し、問題があればログインページに移動させます。
LocalStorageに保存するデータは、あくまでコンポーネント表示の可否を決めるだけです。SPAとサーバーとの通信では、セッション変数などで通信毎に認証状態の確認を行う必要があります。また、ログインしないと表示されないつもりでスクリプト内に非ログインユーザーに見られてはいけないデータを記述するのも危険です。ソースコードから読みだされてしまいます。
さらに、サーバーとの通信時にログアウトや状態異常やを検知した場合は、LocalStorageに設定した値もクリアするようにします。
これらの実装はあとから行うこととして、ここでは利用期限のチェックのみGuardに追記します。
guard-name.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class YourGuard implements CanActivate {
constructor(private router: Router) {}
canActivate{
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean | UrlTree {
let limit: any = localStorage.getItem('token');
if (!isNaN(limit) && Date.now() < limit) {
return true;
} else {
return this.router.parseUrl('login-page?url='+state.url);
}
}
}
ガードにパラメータを渡したい場合
canActiveルーターの設定時、ガードにパラメータを渡したい場合は、 StackOverflow:「How to pass parameters to constructor of canActivate?」にあるように、dataプロパティを使います。
app-routing.module.ts
...
const routes: Routes = [
{ path: 'secretpage', component: SecretComponent, canActivate: [Guard],data:{ validrange: [1,2,3] } },
...
ガードのインスタンスでは、ActivatedRouteSnapshotのプロパティとして設定したデータが受け取れます。
guard.ts
...
canActivate(route: ActivatedRouteSnapshot): boolean {
console.log(route.data.validrange);//[1,2,3]
...
}
...
ページの移動を防止するガード
ログインの為のガードは前述の通りですが、もうひとつよく使うガードの紹介をします。CanDeactivateガードを使うと編集中にページが移動されようとした際にそれを防ぐことができます。
ちなみに、JavaScriptでページのリロードや遷移を防止する方法に、
window.addEventListener('beforeunload', event=> {
event.returnValue = true;
});
というコードがありますが、これはAngularのルーティング内では動きません。また逆に、後述するCanDeactivateガードはリロードやルーティング外のページ移動では動きません。
作成は先ほど同様に「generate guard」で作成し、選択肢の部分でCanDeactivateを選びます。ここではmove-pageという名前で作成しました。
ng generate guard move-page ( ) CanActivate ( ) CanActivateChild >(*) CanDeactivate ( ) CanLoad
CanActivateと似たような流れになっていて、中のメソッドでtrueを返すとページが移動でき、falseを返すとページ遷移を中断、UrlTreeを返すとそのページへ移動します。
.tsファイル内の、ジェネリクスと、componentプロパティでunknownとなっているところをanyもしくは特定のコンポーネントクラスにします。ComopnentではcanDeactivateメソッドを実装するようにし、各コンポーネントからガードに値を渡すようにします。
move-page.guard.ts
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class MovePageGuard implements CanDeactivate<any> {
canDeactivate(
component: any,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState?: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return component.canDeactivate ? component.canDeactivate() : true;
}
}
CanDeactivateを設定させたいコンポーネントのルーティングの設定の中に「canDeactivate:[MovePageGuard]」を追加します。
app-routing.module.ts
...
const routes: Routes = [
{ path: 'test', component: TestComponent,canDeactivate:canDeactivate:[MovePageGuard]},
...
CanDeactivateの処理とは関係ないですが、ユーザーにページ移動の同意を求めるための確認ダイアログのコンポーネントをAngular Materialのダイアログで作成します。ここではcommonoDialogとしました。
common-dialog.component.ts
import { Component, Inject, OnInit } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
@Component({
selector: 'app-common-dialog',
templateUrl: './common-dialog.component.html',
styleUrls: ['./common-dialog.component.css']
})
export class CommonDialogComponent implements OnInit {
constructor(
public dialogRef: MatDialogRef<Component>,
@Inject(MAT_DIALOG_DATA) public data: string[]
) {}
ngOnInit(): void {
}
buttonClick(bln:boolean):void {
this.dialogRef.close(bln);
}
}
common-dialog.component.html
<div mat-dialog-content>
<p>{{data[0]}}</p>
</div>
<div mat-dialog-actions>
<button mat-button (click)="buttonClick(true)">{{data[1]}}</button>
<button mat-button (click)="buttonClick(false)" cdkFocusInitial>{{data[2]}}</button>
</div>
ガードを設定するこのコンポーネントで、ダイアログを呼び出して結果を取得し、それをガードに渡します。
呼び出しのためにMatDialogをコンストラクタに設定します。
test.component.ts
import { MatDialog } from '@angular/material/dialog';
constructor(
...
public dialog: MatDialog
) { }
...
public canDeactivate():Observable<boolean> {
let params:string[]=[
'入力を中止してページを移動します。よろしいですか?',
'はい',
'いいえ'
];
const dialogRef= this.dialog.open(CommonDialogComponent,{width: "500px", data: params});
return dialogRef.afterClosed();
}
呼び出したダイアログの戻り値をbooleanに設定してあるのでそのままガードに渡していますが、何かしらの変換が必要ならafterClosed()に続けてObservableのpipeを使います。AngularにおけるObservableについてはリンク先のページでも説明しているのでそちらも合わせて読んでいただけたら幸いです。
参考にさせていただきましたサイトの皆様、ありがとうございました。