Angular Tutorial

Authentication with Firebase

Revised on 2021/12/18

Angular v13.0.0

@angular/fire v7.2.0

firebase v9.4.0

登入認證方式

  • Email/Password登入
    • 帳號/密碼儲存於雲端資料庫
      • 可使用Firebase Authentication功能
         
  • 社群網站帳號登入
    • Google登入
      • 必須在Google後台設定「伺服器」
    • Facebook登入
      • 必須在FB for Developer後台設定「FB應用程式」
  • 登入狀態檢查
    • HTML 5 local storage

三步驟使用Firebase Email/Password登入服務

(1)啟用Firebase Authentication的Email認證服務
(前往  https://firebase.google.com/​

 

(2) 呼叫firebase提供的API撰寫「登入/註冊/登出」功能

 

 

(3) 在頁面使用

引入服務、叫用「登入/註冊/登出」功能

ng g service services/auth

例:

步驟一:啟用Firebase認證服務

(前往  https://firebase.google.com/​

啟用Email認證服務新增firebase專案(1/4)

啟用Email認證服務啟用登入方式供應商(2/4)

進入專案後...

選擇要啟用的供應商

需符合各供應商的登入連線要求

啟用Email認證服務啟用電子郵件登入(3/4)

啟用Email認證服務建立登入帳號(4/4)

直接輸入電子郵件與密碼

步驟二之一

建立專案,建立服務,安裝套件

 

ng g service services/auth

建立服務:

需安裝套件: @angular/fire,安裝過程一併安裝firebase

 

專案需使用「認證模組」:AngularFireAuthModule

登入頁面範例建立專案

ng g c home
ng g c login

1.專案內新增所需頁面(if necessary)

HomeComponent
    已登入時,則至首頁
LoginComponent

    未登入時,則顯示登入畫面

 

AuthService

    連結Firebase進行登入驗證

ng g service service/auth

2.新增登入認證service

Firebase API提供「可訂閱資料流」: AuthState用以記錄登入狀態之變化

登入頁面範例其他準備事項

☐ Bootstrap設定: 修改 index.html

      加裝其他bootstrap套件(if necessary)

☐ 路徑設定: 修改app-routing.module.ts

加入首頁、與各頁面設定

☐ 首頁內容設定: 修改app.component.ts          

僅留下<router-outlet></router-outlet>

☐ 修改app.module.ts(以加裝ng bootstrap套件為例)

ng add @angular/fire

(3-1) 選擇要加入(app.module.ts)的功能

Authentication(認證服務)

Firestore(資料庫)

Y

登入Google帳戶,於畫面中勾選允許存取

(3-2) 讓firebase CLI取得權限

3.安裝firebase, @angular/fire套件

firebase: v. 9.4.0

@angular/fire: v. 7.2.0

2021/12/11

(3-3) 複製授權碼,貼到下一個問題

(3-4) 選擇Firebase主控台開設之專案

(3-5) 選擇專案中已設定之應用程式

授權碼

(3-5-1) 若無應用程式,到Firebase主控台進行新增

選取「網頁」平台

任何名稱皆可

不要勾選

輸入暱稱後即可「註冊應用程式」

(3-6) 完成後,會自動更新下列檔案

//...
export const environment = {
  firebase: {
    projectId: 'mycrm-1267b',
    appId: '1:78285782410:web:3025241ba471be3aacdc73',
    storageBucket: 'mycrm-1267b.appspot.com',
    locationId: 'us-central',
    apiKey: 'AIzaSyBmazF-4abfCjIakVQiTsjHWKQ0DZ0e6JI',
    authDomain: 'mycrm-1267b.firebaseapp.com',
    messagingSenderId: '78285782410',
  },
  production: false
};

src/environments/environment.ts 片段

(3-6-1) 確認Firebase應用程式連線設定已加入(自動加入)

...
import { AngularFireModule } from '@angular/fire/compat';
import { environment } from '../environments/environment';
import { AngularFireAuthModule } from '@angular/fire/compat/auth';
import { AngularFirestoreModule } from '@angular/fire/compat/firestore';
...
  imports: [
    AngularFireModule.initializeApp(environment.firebase),
    AngularFireAuthModule,
    AngularFirestoreModule
  ],
 ...

app.module.ts(修改後)

(3-6-2)務必修改專案主要模組設定檔(angularfire v.7.x.x仍有問題)

...
import { initializeApp,provideFirebaseApp } from '@angular/fire/app';
import { environment } from '../environments/environment';
import { provideAuth,getAuth } from '@angular/fire/auth';
import { provideFirestore,getFirestore } from '@angular/fire/firestore';
    ...
   imports: [
    ...
    provideFirebaseApp(() => initializeApp(environment.firebase)),
    provideAuth(() => getAuth()),
    provideFirestore(() => getFirestore())
   ],
...

app.module.ts(修改前)

步驟二之二

使用AngularFireAuthModule功能,開始撰寫「登入/註冊/登出」服務函式

 

於服務功能檔:auth.service.ts 中加入各種函式

撰寫服務功能import使用的元件 (1/5)

...
import { AngularFireAuth } from '@angular/fire/compat/auth';
...

auth.service.ts片段

(4-0) 手動import AngularFireAuth元件

該元件提供Firebase認證功能函式

注意: 此為@angular/fire v.7.2.0的最新寫法

(若為v. 6.x.x之前版本,另有不同寫法)

檢查專案的package.json內容,可知版本

撰寫服務功能釐清需求 (2.0/5)

登入需求:

傳入帳密

帳密正確?

前往首頁
紀錄登入資訊

顯示錯誤
停留在此頁

需要Router服務

需要Web Storage

登出需求:

需要Firebase Auth服務

移除登入資訊
前往login頁面

需要Router服務

需要Web Storage

需要Firebase Auth服務

以登入/登出功能為例:

安裝web storage套件

Yes

No

撰寫服務功能安裝web storage套件(2.1/5)

npm install ngx-webstorage
import {NgxWebstorageModule} from 'ngx-webstorage';

@NgModule({
	declarations: [...],
	imports: [
		...
		NgxWebstorageModule.forRoot(),
        ...
	],
	bootstrap: [...]
})
export class AppModule {
}

1. 安裝套件

2. 加入功能模組

app.module.ts片段

撰寫服務功能基本架構 (3/5)

...
export class AuthService {

  constructor(public auth: AngularFireAuth) { }  /*注意必須設定為public*/
  
  // 功能1. 登入(email登入)
  login(mail: string, passwd: string) {
    this.auth.signInWithEmailAndPassword(email, passwd).then().catch();
  }

  // 功能2. 登出(所有登出)
  logout(): void {
    this.auth.signOut().then().catch();
  }
}

auth.service.ts片段

基本架構:透過this.auth選出「登入」/「登出]函式

signInWithEmailAndPassword()

signOut()

撰寫服務功能登入 (4/5)

export class AuthService {
  constructor(public auth: AngularFireAuth, /* 注意auth必須設定為public */
    public router: Router,  private storage: LocalStorageService
    ) { 
     // 訂閱firebase服務提供的「登入狀態」資料流
    this.auth.authState.subscribe(user => {
      if (user) {
        this.user = user;
        this.storage.store('user', this.user);// 寫入local storage
      } 
    });
  }  
  // 功能1. 登入(email登入)
  login(email: string, passwd: string) {
    this.auth.signInWithEmailAndPassword(email, passwd).then(async (uc) => {
      console.log(uc.user);               // 印出登入成功收到的物件
      // await this.setUserData(uc.user); // 將資料寫入資料庫user集合
      this.router.navigate(['/home']);    // 跳轉至home頁面
    }).catch(error => {
      console.log('帳密輸入錯誤', error);	 // 登入失敗
    });
  }
}

auth.service.ts片段

登入/登出之外,需搭配訂閱authState資料流(同樣透過this.auth選出)

...
export class AuthService {
  // 是否已登入
  isLoggedIn(): boolean {
    const user = this.storage.retrieve('user');
    return (user !== null) ? true : false;
  }
  // 功能2:登出
  logout(): void {
    this.auth.signOut().then(() => {
      this.storage.clear('user');  			// 清除user資訊
      console.log('登出成功');
      this.router.navigate(['/login']);  	// 跳至login頁面
    }).catch(error => {
      console.log('登出失敗')
    });
  }

}

auth.service.ts片段

撰寫服務功能登出 (5/5)

訂閱authState資料流:負責登入時,將user存入local storage

登出功能:負責登出時,將user從local storage移除

isLoggedIn():檢查local storage裡是否有user紀錄

步驟三

使用AuthService登入/登出功能,撰寫頁面

 

頁面:login.component.ts

login.component.html

登入頁面範例登入頁面(1/3)

<div class="container">
    <div class="row justify-content-center">
        <div class="col-lg-5 col-md-7">
            <div class="card bg-secondary border-0 mb-0">             
                <div class="card-body px-lg-5 py-lg-5">                    
                    <form role="form" (ngSubmit)="signin()">
                        <div class="form-group mb-3">
                            <div class="input-group input-group-merge input-group-alternative">
                                <div class="input-group-prepend">
                                    <span class="input-group-text"><i class="ni ni-email-83"></i></span>
                                </div>
                                <input class="form-control" [(ngModel)]="email" name="email" placeholder="電子郵件"
                                    type="email">
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="input-group input-group-merge input-group-alternative">
                                <div class="input-group-prepend">
                                    <span class="input-group-text"><i class="ni ni-lock-circle-open"></i></span>
                                </div>
                                <input class="form-control" [(ngModel)]="password" name="password" placeholder="密碼"
                                    type="password">
                            </div>
                        </div>
                        <div class="custom-control custom-control-alternative custom-checkbox">
                            <input class="custom-control-input" id=" customCheckLogin" type="checkbox">
                            <label class="custom-control-label" for=" customCheckLogin">
                                <span class="text-muted">記住我</span>
                            </label>
                        </div>
                        <div class="text-center">
                            <button type="submit" class="btn btn-primary my-4">登入</button>
                        </div>
                    </form>
                </div>
            </div>
            <div class="row mt-3">
                <div class="col-6">
                    <a href="#" class="text-light"><small>忘記密碼?</small></a>
                </div>
                <div class="col-6 text-right">
                    <a [routerLink]="['/register']" class="text-light"><small>註冊新帳號</small></a>
                </div>
            </div>
        </div>
    </div>
</div>

login.component.html (第一版, 使用ngModel)

登入頁面範例登入頁面(2/3)

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../service/auth.service';
//...省略...
export class LoginComponent implements OnInit {
  email='';
  password=''

  constructor(public as: AuthService, public router: Router) { }

  ngOnInit(): void {
    if (this.as.isLoggedIn()) {
      console.log('已登入', this.as.user);
      this.router.navigate(['/home']);
    }
  }

  signin(){
    this.as.login(this.email, this.password);
  }

}

login.component.ts 片段

登入頁面範例登入頁面:資料模型(3/3)

export interface User {
    uid: string;
    email: string;
    displayName: string;
    photoURL: string;
    emailVerified: boolean;
}

在src資料夾建立model/user.ts

此五個欄位是Firebase內建的五個欄位

(Google登入可使用)

登入頁面範例首頁 (1/2)

<!-- Main content -->
<div class="main-content" id="panel">
    <!-- Topnav -->
    <nav class="navbar navbar-top navbar-expand navbar-dark bg-default border-bottom">
        <div class="container-fluid">
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <!-- Navbar links -->

                <ul class="navbar-nav align-items-center  ml-auto ml-md-10 ">
                    <li class="nav-item dropdown">
                        <a class="nav-link pr-0" href="#" role="button" data-toggle="dropdown" aria-haspopup="true"
                            aria-expanded="false">
                            <div class="media align-items-center">
                                <span class="avatar avatar-sm rounded-circle">
                                    <img alt="Image placeholder" [src]="user.photoURL? user.photoURL: pseudo_avatar">
                                </span>
                                <div class="media-body  ml-2  d-none d-lg-block">
                                    <span class="mb-0 text-sm  font-weight-bold">{{user.displayName}}</span>
                                </div>
                            </div>
                        </a>
                        <div class="dropdown-menu  dropdown-menu-right ">
                            <div class="dropdown-header noti-title">
                                <h6 class="text-overflow m-0">Welcome!</h6>
                            </div>
                            <a href="#!" class="dropdown-item">
                                <i class="ni ni-single-02"></i>
                                <span>My profile</span>
                            </a>
                            <a href="#!" class="dropdown-item">
                                <i class="ni ni-settings-gear-65"></i>
                                <span>Settings</span>
                            </a>
                            <a href="#!" class="dropdown-item">
                                <i class="ni ni-calendar-grid-58"></i>
                                <span>Activity</span>
                            </a>
                            <a href="#!" class="dropdown-item">
                                <i class="ni ni-support-16"></i>
                                <span>Support</span>
                            </a>
                            <div class="dropdown-divider"></div>
                            <a href="#!" class="dropdown-item">
                                <i class="ni ni-user-run"></i>
                                <span>Logout</span>
                            </a>
                        </div>
                    </li>
                </ul>
            </div>
            <button (click)="signout()" class="btn btn-success">登出</button>
        </div>
    </nav>
    <!-- Header -->
    <!-- Header -->
    <div class="header pb-6 d-flex align-items-center"
        style="min-height: 500px; background-image: url(../assets/img/theme/profile-cover.jpg); background-size: cover; background-position: center top;">
        <!-- Mask -->
        <span class="mask bg-gradient-default opacity-8"></span>
        <!-- Header container -->
        <div class="container-fluid d-flex align-items-center">
            <div class="row">
                <div class="col-lg-7 col-md-10">
                    <h1 class="display-2 text-white">Hello {{user.displayName}}</h1>
                    <p class="text-white mt-0 mb-5">This is your profile page. You can see the progress you've made with
                        your work and manage your projects or assigned tasks</p>
                    <a href="#!" class="btn btn-neutral">Edit profile</a>
                </div>
            </div>
        </div>
    </div>
</div>

home.component.html (第一版)

登入頁面範例首頁 (2/2)

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { LocalStorageService } from 'ngx-webstorage';
import { User } from '../model/user';
import { AuthService } from '../service/auth.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
  user: User;
  pseudo_avatar = "/assets/img/icons/user-solid.svg"

  constructor(public router: Router, public as: AuthService, 
              public storage: LocalStorageService) { 
    this.user = <User>this.storage.retrieve('user'); 
  }

  ngOnInit(): void { }

  signout() {
    this.as.logout();
  }
}

home.component.ts (第一版)

步驟四:使用Angular Route Guard

增加路徑守衛(route guard):管理頁面的存取權限

 

☐ 在app-routing.module.ts加上設定

問題: 直接輸入網址 localhost/home 不論有無登入,還是可以直接造訪

ng g guard guard/auth

Route Guard路徑守衛 (1/3)

1. 建立authGuard服務

實作哪些介面? 選擇 CanActivate(設定頁面啟動的條件)

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';

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

}

guard/auth.guard.ts

程式碼產生如下

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

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  
  constructor(
    public as: AuthService,
    public router: Router
  ){}
  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    if(this.as.isLoggedIn() !== true) {
      this.router.navigate(['/login']); // 未登入,導向login
    }
      return true;
  }
  
}

auth.guard.ts

Route Guard路徑守衛 (2/3)

2. 設定何種情況下,某個頁面可以啟動

未登入 ➡ 轉向login頁面

已登入➡ 回傳true

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './guard/auth.guard';
import { HomeComponent } from './home/home.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';

const routes: Routes = [
  { path: '', redirectTo: '/home', pathMatch:'full'},
  { path: 'home', component: HomeComponent, canActivate: [AuthGuard]},
  { path: 'login', component: LoginComponent},
  { path: 'register', component: RegisterComponent}
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

app-routing.module.ts

Route Guard路徑守衛 (3/3)

3. 在需要路徑守衛的頁面處,加入canActivate設定

步驟五:增加註冊功能

☑ 撰寫註冊服務(auth.service.ts)

使用AuthService註冊功能,撰寫頁面

 

註冊註冊服務 (1/7)

...
export class AuthService {
  ....
  // 功能3. 註冊新使用者
  register(email: string, passwd: string) {
    this.auth.createUserWithEmailAndPassword(email, passwd).then(
      async (result) => {
        await this.setUserData(result.user);
        this.logout();		// 註冊完後, 登出再重新登入
      }).catch(error => {
        console.log('註冊失敗');
      });
  }
  ...
}

auth.service.ts片段

此註冊功能只先提供mail與密碼

註冊註冊頁面 (2/7)

import { Component, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import Validation from '../helpers/confirm.validator';
import { AuthService } from '../service/auth.service';
....
export class RegisterComponent implements OnInit {
  registerForm: FormGroup;  // 表單
  get form(): { [key: string]: AbstractControl } {
    return this.registerForm.controls;
  }
  submitted = false;    // 是否已送出?

  constructor(private builder: FormBuilder, 
    public router: Router, public as: AuthService) { 
    this.registerForm = this.buildForm();
  }

  ngOnInit(): void {
  }
  
  //使用FormBuilder服務建立表單
  private buildForm(): FormGroup {
    return new FormGroup({
      email : new FormControl('',[Validators.required, Validators.email]),
      password: new FormControl('', [Validators.required, Validators.minLength(8)]),
      confirm : new FormControl('', Validators.required)
    }, Validation.ConfirmedValidator('password', 'confirm'));
  }
  // ....未完,續下一頁...
}

register.component.ts片段(1/2)

ng g c resgister

建立註冊頁面(if necessary)

註冊表單改用ReactiveFormsModule

而非先前介紹的ngModel (FormsModule)

ReactiveFormsModule 大多工作都在TS檔完成

註冊註冊頁面 (3/7)

import { Component, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import Validation from '../helpers/confirm.validator';
import { AuthService } from '../service/auth.service';
....
export class RegisterComponent implements OnInit {
  // ...續上一頁(2/2)
  // ...
  onReset(): void {
    this.submitted = false;
    this.registerForm.reset();
  }
  register() {
    this.submitted = true;
    if (this.registerForm.invalid) {
      return;
    }
    const email = this.registerForm.get('email')?.value;
    const password = this.registerForm.get('password')?.value; 
    this.as.register(email, password);
  }
}

register.component.ts片段(2/2)

import { AbstractControl, ValidatorFn } from '@angular/forms';

export default class Validation {
    static ConfirmedValidator(controlName: string, matchingControlName: string): ValidatorFn {
        return (controls: AbstractControl) => {
            const control = controls.get(controlName);     // 第一個欄位
            const matchingControl = controls.get(matchingControlName); // 比對欄位
    
            if (control === null || matchingControl === null) {
                return null;
            }
            if (matchingControl.errors && !matchingControl.errors.confirmedValidator) {
                // 若有其他錯誤,則直接return
                return null;
            }
    
            // 根據比對結果  設定matchingControl錯誤狀態 
            if (control.value !== matchingControl.value) {  // 兩者不相等
                controls.get(matchingControlName)?.setErrors({matching: true});
                return { matching: true};
            } else {
                return null;
            }
        }
    }
}

手動新增helpers/confirm.validator.ts

提供ConfirmedValidator函式:比對兩個欄位的內容是否相同

helpers/confirm.validator.ts

註冊密碼比對功能 (4/7)

return自訂認證錯誤類型:

    matching: true

註冊註冊頁面 (5/7)

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

...

@NgModule({
  imports: [
    ...
    FormsModule,
    ReactiveFormsModule,
    ...
  ],
 ...
})
export class AppModule {}

app.module.ts

為了在頁面使用表單,必須引入FormsModule, ReactiveFormsModule

註冊註冊頁面 (6/7)

<form [formGroup]="registerForm" role="form" (ngSubmit)="register()">
            <!--...電子郵件欄位-->
            <input class="form-control" formControlName="email" placeholder="電子郵件"
                [ngClass]="{ 'is-invalid': submitted && form.email.errors }" type="email">
            <div *ngIf="submitted && form.email.errors" class="invalid-feedback">
                <div *ngIf="form.email.errors.required">必填欄位</div>
                <div *ngIf="form.email.errors.email">請照電子郵件格式填入</div>
            </div>
            ...
            <!--...密碼欄位-->
            <input class="form-control" formControlName="password" placeholder="密碼"
                [ngClass]="{ 'is-invalid': submitted && form.password.errors }" type="password">
            <div *ngIf="submitted && form.password.errors" class="invalid-feedback">
                <div *ngIf="form.password.errors.required">必填欄位</div>
                <div *ngIf="form.password.errors.minlength">密碼最少8碼</div>
            </div>
            ...
            <!--...再次輸入密碼-->
            <input class="form-control" formControlName="confirm" placeholder="再次確認密碼"
                [ngClass]="{ 'is-invalid': submitted && form.confirm.errors }" type="password">
            <div *ngIf="submitted && form.confirm.errors" class="invalid-feedback">
                <div *ngIf="form.confirm.errors.required">必填欄位</div>
                <div *ngIf="form.confirm.errors.matching">重新輸入密碼不符</div>
            ...
    <div class="text-center">
        <button type="submit" class="btn btn-primary my-4">註冊新帳號</button>
        <button type="button" (click)="onReset()" class="btn btn-warning my-4">取消</button>
    </div>
</form>

register.component.html 片段

<form [formGroup]="registerForm" role="form" (ngSubmit)="register()">
     <!-- 各個欄位 -->
</form>

Reactive Form 必須在<form> 設定formGroup屬性(屬性繫結)

事件繫結ngSubmit 綁定TS檔的register()函式

import { Component, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import Validation from '../helpers/confirm.validator';
import { AuthService } from '../service/auth.service';
....
export class RegisterComponent implements OnInit {
  registerForm: FormGroup;  // 表單
  // ....
  // 
  register() {
    this.submitted = true;
    if (this.registerForm.invalid) {
      return;
    }
    const email = this.registerForm.get('email')?.value;
    const password = this.registerForm.get('password')?.value; 
    this.as.register(email, password);
  }
}

register.component.html 片段

register.component.ts 片段

<form [formGroup]="registerForm" role="form" (ngSubmit)="register()">
            <!--...電子郵件欄位-->
            <input class="form-control" formControlName="email" placeholder="電子郵件"
                [ngClass]="{ 'is-invalid': submitted && form.email.errors }" type="email">
            ...
</form>

register.component.html 片段

Reactive Form 欄位(如<input>) 都必須設定formControlName屬性(屬性繫結)

import { Component, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import Validation from '../helpers/confirm.validator';
import { AuthService } from '../service/auth.service';
....
export class RegisterComponent implements OnInit {
  registerForm: FormGroup;  // 表單
  
  constructor(private builder: FormBuilder, 
    public router: Router, public as: AuthService) { 
    this.registerForm = this.buildForm();
  }
  
  private buildForm(): FormGroup {
    return new FormGroup({
      email : new FormControl('',[Validators.required, Validators.email]),
      password: new FormControl('', [Validators.required, Validators.minLength(8)]),
      confirm : new FormControl('', Validators.required)
    }, Validation.ConfirmedValidator('password', 'confirm'));
  }
  //...
}

register.component.ts 片段

例如:"email"定義於TS檔第17行

<form [formGroup]="registerForm" role="form" (ngSubmit)="register()">
            <!--...電子郵件欄位-->
            <input class="form-control" formControlName="email" placeholder="電子郵件"
                [ngClass]="{ 'is-invalid': submitted && form.email.errors }" type="email">
            ...
</form>

register.component.html 片段

is-invalid為Bootstrap類別

submitted, form都定義於TS檔

import { Component, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import Validation from '../helpers/confirm.validator';
import { AuthService } from '../service/auth.service';
....
export class RegisterComponent implements OnInit {
  registerForm: FormGroup;  // 表單
  get form(): { [key: string]: AbstractControl } {
    return this.registerForm.controls;
  }
  submitted = false;    // 是否已送出?
 
  //...
}

register.component.ts 片段

[ngClass]="{ 'CSS類別名稱': 條件測試 }"

get 屬性名稱(): 回傳型別{

    函式內容

}

<form [formGroup]="registerForm" role="form" (ngSubmit)="register()">
            <!--...電子郵件欄位-->
            <input class="form-control" formControlName="email" placeholder="電子郵件"
                [ngClass]="{ 'is-invalid': submitted && form.email.errors }" type="email">
            <div *ngIf="submitted && form.email.errors" class="invalid-feedback">
                <div *ngIf="form.email.errors.required">必填欄位</div>
                <div *ngIf="form.email.errors.email">請照電子郵件格式填入</div>
            </div>
</form>

register.component.html 片段

invalid-feedback為Bootstrap類別: 加紅框與文字

form.email.errors內含「認證錯誤類型」是否發生的旗標

register.component.ts 片段

認證錯誤類型:

    required: 必填

    email: 需符合email格式

    minLength(n):  最少n碼

....
export class RegisterComponent implements OnInit {
  //...
  private buildForm(): FormGroup {
    return new FormGroup({
      email : new FormControl('',[Validators.required, Validators.email]),
      password: new FormControl('', [Validators.required, Validators.minLength(8)]),
      confirm : new FormControl('', Validators.required)
    }, Validation.ConfirmedValidator('password', 'confirm'));
  }
  //...
}

註冊註冊頁面 (7/7)

<div class="container">
    <div class="row justify-content-center">
        <div class="col-lg-5 col-md-7">
            <div class="card bg-secondary border-0 mb-0">
                <div class="card-header bg-transparent pb-5">
                    <div class="text-muted text-center mt-2 mb-3"><small>電子郵件註冊</small></div>
                </div>
                <div class="card-body px-lg-5 py-lg-5">
                    <form [formGroup]="registerForm" role="form" (ngSubmit)="register()">
                        <div class="form-group mb-3">
                            <div class="input-group input-group-merge input-group-alternative">
                                <div class="input-group-prepend">
                                    <span class="input-group-text"><i class="ni ni-email-83"></i></span>
                                </div>
                                <input class="form-control" formControlName="email" placeholder="電子郵件"
                                    [ngClass]="{ 'is-invalid': submitted && form.email.errors }" type="email">
                                <div *ngIf="submitted && form.email.errors" class="invalid-feedback">
                                    <div *ngIf="form.email.errors.required">必填欄位</div>
                                    <div *ngIf="form.email.errors.email">請照電子郵件格式填入</div>
                                </div>
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="input-group input-group-merge input-group-alternative">
                                <div class="input-group-prepend">
                                    <span class="input-group-text"><i class="ni ni-lock-circle-open"></i></span>
                                </div>
                                <input class="form-control" formControlName="password" placeholder="密碼"
                                    [ngClass]="{ 'is-invalid': submitted && form.password.errors }" type="password">
                                <div *ngIf="submitted && form.password.errors" class="invalid-feedback">
                                    <div *ngIf="form.password.errors.required">必填欄位</div>
                                    <div *ngIf="form.password.errors.minlength">密碼最少8碼</div>
                                </div>
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="input-group input-group-merge input-group-alternative">
                                <div class="input-group-prepend">
                                    <span class="input-group-text"><i class="ni ni-lock-circle-open"></i></span>
                                </div>
                                <input class="form-control" formControlName="confirm" placeholder="再次確認密碼"
                                    [ngClass]="{ 'is-invalid': submitted && form.confirm.errors }" type="password">
                                <div *ngIf="submitted && form.confirm.errors" class="invalid-feedback">
                                    <div *ngIf="form.confirm.errors.required">必填欄位</div>
                                    <div *ngIf="form.confirm.errors.matching">重新輸入密碼不符</div>
                                </div>
                            </div>
                        </div>
                        <div class="text-center">
                            <button type="submit" class="btn btn-primary my-4">註冊新帳號</button>
                            <button type="button" (click)="onReset()" class="btn btn-warning my-4">取消</button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>

register.component.html 完整

Google登入

  • Firebase端設定

Google帳號登入

1. 登入按鈕

2. 彈出OAuth對話框

3. 收取後續存取資訊

4. 送出認證碼到伺服器

5. 認證碼換取後續存取權杖

6. google回傳後續存取權杖

7.伺服器確認登入

Firebase伺服器

你的網站

Google登入頁面範例 auth service

//...
import firebase from '@firebase/app-compat';
//...
export class AuthService {
  // ...
  loginWithGoogle() {
    return this.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider()).then( 
    async(uc) => {
      console.log(uc);
      await this.setUserData(uc.user); // 將資料寫入資料庫user集合
      this.router.navigate(['/home']);
    }).catch();
  }

  loginWithFacebook() {
    return this.auth.signInWithPopup(new firebase.auth.FacebookAuthProvider()).then(
    async(uc) => {
      console.log(uc);
      await this.setUserData(uc.user); // 將資料寫入資料庫user集合
      this.router.navigate(['/home']);
    }).catch();
  }
  ...
}

auth.service.ts片段

Google登入頁面範例登入頁面html檔

<div class="container">
    <div class="row justify-content-center">
        <div class="col-lg-5 col-md-7">
            <div class="card bg-secondary border-0 mb-0">             
              <div class="card-header bg-transparent pb-5">
                    <div class="text-muted text-center mt-2 mb-3"><small>社群網站登入</small></div>
                    <div class="btn-wrapper text-center">
                        <a (click)="signinWithFacebook()" class="btn btn-neutral btn-icon">
                            <span class="btn-inner--icon"><img src="../assets/img/icons/common/facebook.svg"></span>
                            <span class="btn-inner--text">Facebook</span>
                        </a>
                        <a (click)="signinWithGoogle()" class="btn btn-neutral btn-icon">
                            <span class="btn-inner--icon"><img src="../assets/img/icons/common/google.svg"></span>
                            <span class="btn-inner--text">Google</span>
                        </a>
                    </div>
                </div>
                <div class="card-body px-lg-5 py-lg-5">                    
                    <form role="form" (ngSubmit)="signin()">
                        <div class="form-group mb-3">
                            <div class="input-group input-group-merge input-group-alternative">
                                <div class="input-group-prepend">
                                    <span class="input-group-text"><i class="ni ni-email-83"></i></span>
                                </div>
                                <input class="form-control" [(ngModel)]="email" name="email" placeholder="電子郵件"
                                    type="email">
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="input-group input-group-merge input-group-alternative">
                                <div class="input-group-prepend">
                                    <span class="input-group-text"><i class="ni ni-lock-circle-open"></i></span>
                                </div>
                                <input class="form-control" [(ngModel)]="password" name="password" placeholder="密碼"
                                    type="password">
                            </div>
                        </div>
                        <div class="custom-control custom-control-alternative custom-checkbox">
                            <input class="custom-control-input" id=" customCheckLogin" type="checkbox">
                            <label class="custom-control-label" for=" customCheckLogin">
                                <span class="text-muted">記住我</span>
                            </label>
                        </div>
                        <div class="text-center">
                            <button type="submit" class="btn btn-primary my-4">登入</button>
                        </div>
                    </form>
                </div>
            </div>
            <div class="row mt-3">
                <div class="col-6">
                    <a href="#" class="text-light"><small>忘記密碼?</small></a>
                </div>
                <div class="col-6 text-right">
                    <a [routerLink]="['/register']" class="text-light"><small>註冊新帳號</small></a>
                </div>
            </div>
        </div>
    </div>
</div>

login.component.html (第二版, 加入社群登入)

Google登入頁面範例登入頁面ts檔

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../service/auth.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
  email='';
  password=''

  constructor(public as: AuthService, public router: Router) { }

  ngOnInit(): void {
    if (this.as.isLoggedIn()) {
      console.log('已登入', this.as.user);
      this.router.navigate(['/home']);
    }
  }

  signin(){
    this.as.login(this.email, this.password);
  }

  signinWithGoogle() {
    this.as.loginWithGoogle();
  }

  signinWithFacebook() {
    this.as.loginWithFacebook();
  }
}

login.component.ts (第二版, 加入社群登入)

Facebook登入

  • Firebase端設定

Facebook帳號登入

1. 點選登入

2. 彈出OAuth對話框

1. 送出FB登入認證

2. 要求app轉址

3. 進行轉址

4. 要求取得存取權限

5. 回傳存取權杖

6. 要求讀取用戶資料

7. 回傳用戶資料

8. 顯示用戶資料

FB應用程式

FB登入fackbook應用程式設定

前往https://developers.facebook.com/apps/  建立應用程式

FB登入fackbook應用程式設定

FB登入fackbook應用程式設定

複製到Firebase

FB登入fackbook應用程式設定

複製回Facebook

FB登入fackbook應用程式設定

因強制使用HTTPS,故需用https://localhost:4200

從Firebase主控台複製過來的網址

如何設定https://localhost:4200?

Becoming a (Tiny) Certificate Authority

1. 安裝Git for Windows

開啟 Git Bash

2. 以openssl產生私密金鑰(Private Key)

但須透過winpty工具程式執行openssl

winpty openssl genrsa -des3 -out myCA.key 2048

Becoming a (Tiny) Certificate Authority

3. 以openssl產生根憑證(Root Certificate)

winpty openssl req -x509 -new -nodes -key myCA.key -sha256 -days 1825 -out myCA.pem

Becoming a (Tiny) Certificate Authority

4. 找到私密金鑰與根憑證 myCA.key, myCA.pem

Becoming a (Tiny) Certificate Authority

4.1 將私密金鑰 myCA.key 轉成不需密碼的server.key

winpty openssl rsa -in "C:\Users\AU\myCA.key" -out "C:\Users\AU\server.key"

5. 安裝根憑證(透過Windows Management Console)

搜尋並執行mmc

Becoming a (Tiny) Certificate Authority

5. 安裝根憑證(透過Windows Management Console)

Becoming a (Tiny) Certificate Authority

5. 安裝根憑證(透過Windows Management Console)

Becoming a (Tiny) Certificate Authority

5. 安裝根憑證(透過Windows Management Console)

Becoming a (Tiny) Certificate Authority

5. 安裝根憑證(透過Windows Management Console)

Becoming a (Tiny) Certificate Authority

5. 安裝根憑證(透過Windows Management Console)

Becoming a (Tiny) Certificate Authority

5. 安裝根憑證(透過Windows Management Console)

Becoming a (Tiny) Certificate Authority

5. 安裝根憑證(透過Windows Management Console)

Becoming a (Tiny) Certificate Authority

5. 安裝根憑證(透過Windows Management Console)

Becoming a (Tiny) Certificate Authority

5. 安裝根憑證(透過Windows Management Console)

Becoming a (Tiny) Certificate Authority

5. 安裝根憑證(透過Windows Management Console)

Becoming a (Tiny) Certificate Authority

5. 安裝根憑證(透過Windows Management Console)

Becoming a (Tiny) Certificate Authority

6. 以ssl模式啟動ng serve

ng serve --ssl true --ssl-key "C:\Users\AU\server.key" --ssl-cert "C:\Users\AU\myCA.pem" --open

Becoming a (Tiny) Certificate Authority

Angular Tutorial

By Leuo-Hong Wang

Angular Tutorial

Lesson 7: Authentication with Firebase

  • 474