Angular に認証の仕組みを追加する
Angular に認証の仕組みを追加する
以下で作成した認証 API を利用して Angular に認証の仕組みを追加していきます。
サンプルアプリケーションの挙動は以下で確認できます。
今回作成するのは Cookie ベースの認証を利用した SPA アプリケーションです。
認証の実装ステップ
Angular などの SPA で認証を実装する場合、通常以下のステップで作業を進めていきます。
- 各種画面の作成(signup,mypage,login)
- signup 画面でログイン API のコールを行い、Cookie データを取得する。
- mypage などの認証画面に Guard を適用する
- Guard の中で Cookie の検証を行う
- Guard の中で ユーザデータを取得し、グローバルストアに格納する
- logout の実装を追加する。
- Guard の中で Cookie が不正だった場合の強制リダイレクトを実装する
実際のコードは以下の リポジトリからも確認できます。
実装のポイント
各種画面の作成
画面単位でコンポーネントを作成して、ルートの定義に加えていきます。
signup 画面での API コール
API コールは httpClient モジュールを利用して行います。
サンプルコードでは、リアクティブフォームを利用しているため、
this.form.invalid
でフォームのバリデーション確認を行ってから、API の送信を行っています。
import { Component, OnInit } from '@angular/core';
import {FormControl, FormGroup, Validators} from "@angular/forms";
import {HttpClient} from "@angular/common/http";
import {Router, RouterModule} from "@angular/router";
@Component({
selector: 'app-signup-page',
templateUrl: './signup-page.component.html',
styleUrls: ['./signup-page.component.scss']
})
export class SignupPageComponent implements OnInit {
form = new FormGroup({
email: new FormControl("",Validators.required),
lastName: new FormControl("",Validators.required),
firstName: new FormControl("",Validators.required),
password: new FormControl("",Validators.required),
})
constructor(
private http: HttpClient,
private router: Router
) { }
submit(){
if(this.form.invalid){
alert("すべてのフォームを埋めてください。")
return
}
this.http.post("/api/signup",{
...this.form.value
}).subscribe({
next:(r)=>{
return this.router.navigateByUrl("/cookie/mypage")
},
error: ()=>{
alert("エラー: 登録できませんでした。")
}
})
}
}
API コール時に Chrome 検証ツールの Network 欄で、API のレスポンスに SetCookie ヘッダが付与されているのを確認しておきましょう。
Guard の実装
mypage などの認証画面で、認証済みのユーザのみにアクセスを許可するために、Guard を作成します。
Guard は、ルートと紐付けてルートアクセス時にフィルタ処理を実行できる Angular の機構です。
Guard の中では、ユーザのログイン状態を管理する必要があり、これを実現するためにまず、AuthService を作成しましょう。
import { Injectable } from '@angular/core';
import {BehaviorSubject} from "rxjs";
interface User{
email: string,
firstName: string,
lastName: string,
}
@Injectable({
providedIn: 'root'
})
export class AuthService {
private _user$
constructor() {
this._user$ = new BehaviorSubject<User|null>(null)
}
get user$(){
return this._user$.asObservable()
}
login(user:User){
this._user$.next(user)
}
logout(){
this._user$.next(null)
}
}
AuthService に _user$
を用意して、アプリケーション全体でユーザ状態の変更を監視できるようにしています。
Subject
は書き込み可能な Observable のような存在で、今回は Subject の中でも直前の値を1つ保持してくれる BehaviorSubject
を利用しています。
AuthService では、login / logout を実装し、next をコールしてユーザ状態の変更を通知できるようにしています。
外部に公開する際には getter の user$
で Observable に変換してから公開し、外部からの自由なユーザ情報変更を行えないようにしています。
ここまで準備ができたら、作成した AuthService を利用して、AuthGuard を作成します。
AuthGuard の処理は以下のようなステップで実行されます。
- ルートアクセス前に、AuthService の状態を確認する。
- AuthService.user$ の値が空なら プロフィール API にアクセスする
- プロフィール API がエラーなら ゲスト状態とする
- プロフィール API からデータが取得できたら 認証済みとする
- 認証済みの場合、AuthServiceのデータを更新する
- ゲスト状態の場合、ログイン画面にリダイレクト
- 認証済みの場合、true を返してルートアクセスを許可
import { Injectable } from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from '@angular/router';
import {Observable, of} from 'rxjs';
import {AuthService} from "./auth.service";
import {catchError, distinct, map, switchMap, tap} from "rxjs/operators";
import {HttpClient} from "@angular/common/http";
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private auth: AuthService,
private http: HttpClient,
private router: Router
) {
}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.auth.user$.pipe(
distinct(),
switchMap((userOrNull)=>{
if(userOrNull === null){
// AuthService にデータが入っていないとき
return this.http.get<{user:any}>("/api/profile").pipe(
catchError(()=>of({user:null})),
map((result) => result.user)
)
}else{
// AuthService にデータが入っているとき
return of(userOrNull)
}
}),
tap(async (userOrNull)=>{
// データが存在する場合、AuthService のデータを更新
this.auth.login(userOrNull)
}),
switchMap((userOrNull) => {
if(userOrNull === null){
// 認証失敗
return of(this.router.parseUrl(`/cookie/login?redirect_to=${state.url}`))
}else{
// 認証失敗
return of(true)
}
}),
)
}
}
Discussion