Authentication with Firebase
Revised on 2021/12/18
Angular v13.0.0
@angular/fire v7.2.0
firebase v9.4.0
(1)啟用Firebase Authentication的Email認證服務
(前往 https://firebase.google.com/)
(2) 呼叫firebase提供的API撰寫「登入/註冊/登出」功能
(3) 在頁面使用
引入服務、叫用「登入/註冊/登出」功能
ng g service services/auth
例:
進入專案後...
選擇要啟用的供應商
需符合各供應商的登入連線要求
直接輸入電子郵件與密碼
建立專案,建立服務,安裝套件
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 { 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內容,可知版本
登入需求:
傳入帳密
帳密正確?
前往首頁
紀錄登入資訊
顯示錯誤
停留在此頁
需要Router服務
需要Web Storage
登出需求:
需要Firebase Auth服務
移除登入資訊
前往login頁面
需要Router服務
需要Web Storage
需要Firebase Auth服務
以登入/登出功能為例:
安裝web storage套件
Yes
No
npm install ngx-webstorage
import {NgxWebstorageModule} from 'ngx-webstorage';
@NgModule({
declarations: [...],
imports: [
...
NgxWebstorageModule.forRoot(),
...
],
bootstrap: [...]
})
export class AppModule {
}
1. 安裝套件
2. 加入功能模組
app.module.ts片段
...
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()
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片段
訂閱authState資料流:負責登入時,將user存入local storage
登出功能:負責登出時,將user從local storage移除
isLoggedIn():檢查local storage裡是否有user紀錄
使用AuthService登入/登出功能,撰寫頁面
頁面:login.component.ts
login.component.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-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)
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 片段
export interface User {
uid: string;
email: string;
displayName: string;
photoURL: string;
emailVerified: boolean;
}
在src資料夾建立model/user.ts
此五個欄位是Firebase內建的五個欄位
(Google登入可使用)
<!-- 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 (第一版)
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 (第一版)
☐ 增加路徑守衛(route guard):管理頁面的存取權限
☐ 在app-routing.module.ts加上設定
問題: 直接輸入網址 localhost/home 不論有無登入,還是可以直接造訪
ng g guard guard/auth
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
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
3. 在需要路徑守衛的頁面處,加入canActivate設定
☑ 撰寫註冊服務(auth.service.ts)
☑ 使用AuthService註冊功能,撰寫頁面
...
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與密碼
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檔完成
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
return自訂認證錯誤類型:
matching: true
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
<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'));
}
//...
}
<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 完整
1. 登入按鈕
2. 彈出OAuth對話框
3. 收取後續存取資訊
4. 送出認證碼到伺服器
5. 認證碼換取後續存取權杖
6. google回傳後續存取權杖
7.伺服器確認登入
Firebase伺服器
你的網站
//...
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片段
<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 (第二版, 加入社群登入)
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 (第二版, 加入社群登入)
1. 點選登入
2. 彈出OAuth對話框
1. 送出FB登入認證
2. 要求app轉址
3. 進行轉址
4. 要求取得存取權限
5. 回傳存取權杖
6. 要求讀取用戶資料
7. 回傳用戶資料
8. 顯示用戶資料
FB應用程式
複製到Firebase
複製回Facebook
因強制使用HTTPS,故需用https://localhost:4200
從Firebase主控台複製過來的網址
1. 安裝Git for Windows
開啟 Git Bash
2. 以openssl產生私密金鑰(Private Key)
但須透過winpty工具程式執行openssl
winpty openssl genrsa -des3 -out myCA.key 2048
3. 以openssl產生根憑證(Root Certificate)
winpty openssl req -x509 -new -nodes -key myCA.key -sha256 -days 1825 -out myCA.pem
4. 找到私密金鑰與根憑證 myCA.key, myCA.pem
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
5. 安裝根憑證(透過Windows Management Console)
5. 安裝根憑證(透過Windows Management Console)
5. 安裝根憑證(透過Windows Management Console)
5. 安裝根憑證(透過Windows Management Console)
5. 安裝根憑證(透過Windows Management Console)
5. 安裝根憑證(透過Windows Management Console)
5. 安裝根憑證(透過Windows Management Console)
5. 安裝根憑證(透過Windows Management Console)
5. 安裝根憑證(透過Windows Management Console)
5. 安裝根憑證(透過Windows Management Console)
5. 安裝根憑證(透過Windows Management Console)
6. 以ssl模式啟動ng serve
ng serve --ssl true --ssl-key "C:\Users\AU\server.key" --ssl-cert "C:\Users\AU\myCA.pem" --open