在完成後端 JWT 驗證後,前端該如何接收、存儲和攜帶這個 Token?在這篇文章中,我們將介紹如何在 Angular 中實現 JWT 驗證,包括如何處理登入狀態、發送帶有 Token 的 API 請求、保護頁面,還有一些前後端整合時應該注意的細節。這次我們會走得更細,確保每個步驟都解釋得清清楚楚!
在前一篇中,我們討論了 JWT 在後端的應用。JWT 是一種安全且簡單的方式來實現身份驗證,它能讓我們避免頻繁查詢資料庫。這篇文章將重點講解如何在前端處理這些 Token,並如何和後端進行驗證配合。JWT 在前端的任務就是接收伺服器發送的 Token,並在每次 API 請求時攜帶它。
在 Angular 中,我們會用 HttpClient
來發送請求,並在登入成功後將 Token 儲存在 Local Storage 或 State 中。這樣,我們就可以在後續的 API 請求中自動攜帶 Token,讓伺服器知道用戶的身份。
首先,我們會需要建立兩個基本的頁面:LoginComponent 和 HomeComponent,這樣我們可以進行登入、跳轉首頁以及進行後續的 API 請求測試。
你可以使用 Angular CLI 來快速建立這兩個元件:
ng generate component login
ng generate component home
在 login.component.html 中,你可以使用之前設定好的登入頁面。
也可以參考 Day12 掌握 Angular Material 更多元件與表單驗證:讓應用更強大
<!-- login.component.html -->
<div class="centered-container" fxLayout="column" fxLayoutAlign="center center">
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<mat-form-field appearance="fill">
<mat-label>Email</mat-label>
<input matInput formControlName="email" placeholder="輸入你的 Email" />
<mat-error *ngIf="loginForm.controls['email'].hasError('required')">
Email 是必填的
</mat-error>
<mat-error *ngIf="loginForm.controls['email'].hasError('email')">
Email 格式不正確
</mat-error>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>密碼</mat-label>
<input
matInput
type="password"
formControlName="password"
placeholder="輸入你的密碼"
/>
<mat-error *ngIf="loginForm.controls['password'].hasError('required')">
密碼是必填的
</mat-error>
</mat-form-field>
<button
mat-raised-button
color="primary"
type="submit"
[disabled]="!loginForm.valid"
>
登入
</button>
</form>
</div>
當登入成功後,我們會跳轉到 HomeComponent。這是簡單的首頁,顯示一些商品,並且包含搜尋功能和購物車按鈕。
也可以參考 Day14 綜合 Angular Material 和 Flex Layout 製作簡單購物網站首頁
<!-- home.component.html -->
<mat-toolbar color="primary" fxLayout="row" fxLayoutAlign="space-around center">
<div>
<span>毛毛購物</span>
<button mat-icon-button>
<mat-icon>home</mat-icon>
</button>
</div>
<mat-form-field fxFlex="50%">
<mat-label>搜尋商品</mat-label>
<input matInput placeholder="輸入商品名稱" />
</mat-form-field>
<button mat-icon-button>
<mat-icon>shopping_cart</mat-icon>
</button>
</mat-toolbar>
<div fxLayout="row" class="banner">
<h1 fxFlex>本季熱銷商品!買一送一優惠中!</h1>
</div>
<div fxLayout="row" fxLayoutGap="16px" fxLayoutAlign="center">
<mat-card fxFlex="30%">
<mat-card-header>
<mat-card-title>毛毛舒適床!!!柔軟透氣寵物床</mat-card-title>
<mat-card-subtitle>$100</mat-card-subtitle>
</mat-card-header>
<img mat-card-image src="assets/product1.jpg" alt="產品 1" />
<mat-card-actions>
<button mat-raised-button color="primary">加入購物車</button>
</mat-card-actions>
</mat-card>
<mat-card fxFlex="30%">
<mat-card-header>
<mat-card-title>(⁎˃ᆺ˂)萌萌健康糧天然有機寵物飼料</mat-card-title>
<mat-card-subtitle>$200</mat-card-subtitle>
</mat-card-header>
<img mat-card-image src="assets/product2.jpg" alt="產品 2" />
<mat-card-actions>
<button mat-raised-button color="primary">加入購物車</button>
</mat-card-actions>
</mat-card>
<mat-card fxFlex="30%">
<mat-card-header>
<mat-card-title>毛寶潔牙棒-天然口腔清潔零食</mat-card-title>
<mat-card-subtitle>$300</mat-card-subtitle>
</mat-card-header>
<img mat-card-image src="assets/product3.jpg" alt="產品 3" />
<mat-card-actions>
<button mat-raised-button color="primary">加入購物車</button>
</mat-card-actions>
</mat-card>
</div>
要讓登入頁面和首頁能夠跳轉,我們需要設定路由。編輯 app-routing.module.ts,加入 LoginComponent 和 HomeComponent 路徑。
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { HomeComponent } from './home/home.component';
const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'home', component: HomeComponent },
{ path: '', redirectTo: '/login', pathMatch: 'full' }, // 預設重定向到 login 頁面
{ path: '**', redirectTo: '/login' }, // 未匹配的路由重定向到 login 頁面
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
登入成功後,後端會回傳一個 JWT。我們需要將這個 JWT 儲存起來,後續發送 API 請求時可以帶上這個 Token。接下來,我們將一步步來實作如何在前端處理 JWT。
要發送 HTTP 請求,我們需要導入 Angular 內建的 HttpClientModule。確保在 app.module.ts 中導入並註冊它。
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [],
imports: [HttpClientModule],
})
export class AppModule {}
AuthService 是一個服務,負責發送登入請求並接收後端回傳的 JWT。請在 src/app/services/auth.service.ts 中建立這個服務。
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private apiUrl = 'http://localhost:3000'; // 你的後端 API 路徑
constructor(private http: HttpClient) {}
login(username: string, password: string): Observable<any> {
return this.http.post(`${this.apiUrl}/login`, { username, password });
}
}
這個服務會向後端發送 POST 請求,並回傳 JWT。
我們可以在登入成功後,將 JWT 儲存在 Local Storage,以便後續的 API 請求可以自動攜帶這個 Token。這部分的邏輯需要寫在 LoginComponent 的 onSubmit() 方法中。
// src/app/login/login.component.ts
import { Component } from '@angular/core';
import { AuthService } from '../services/auth.service';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css'], // 添加這行來引用 CSS
})
export class LoginComponent {
loginForm: FormGroup;
constructor(
private authService: AuthService,
private router: Router,
private fb: FormBuilder
) {
this.loginForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required],
});
}
onSubmit() {
const { email, password } = this.loginForm.value;
this.authService.login(email, password).subscribe(
(response) => {
localStorage.setItem('token', response.token); // 將 JWT 存入 Local Storage
this.router.navigate(['/home']); // 登入成功後跳轉到首頁
},
(error) => {
console.error('登入失敗', error);
}
);
}
}
這段程式碼在 LoginComponent 中處理登入邏輯,當登入成功時,將 JWT 存入 Local Storage,並跳轉到 HomeComponent。
雖然我們已經將 JWT 儲存在 Local Storage 中,但依賴 Local Storage 的方式有時不夠靈活,尤其當我們希望即時追蹤用戶的登入狀態時。我們可以使用 Angular 的 BehaviorSubject 來將 JWT 存成應用程式的 State,這樣我們可以即時更新應用程式的狀態,而不用每次都從 Local Storage 中讀取。
這樣的做法能讓應用程式中的其他組件即時接收到登入狀態的變化,例如更新導航欄,顯示用戶是否已經登入等。
BehaviorSubject
是 RxJS 提供的一個非常實用的工具,它可以保存當前的狀態,並且當狀態變更時自動通知所有訂閱者。這非常適合用來管理應用程式中的登入狀態。
首先,我們會在 auth.service.ts
中實作一個 BehaviorSubject 來追蹤 JWT Token,並確保在登入或登出時能夠更新這個 State。
auth.service.ts
:import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private apiUrl = 'http://localhost:3000'; // API 伺服器路徑
private tokenSubject = new BehaviorSubject<string | null>(null); // 用來追蹤 JWT Token 的狀態
constructor(private http: HttpClient) {
const token = localStorage.getItem('token');
if (token) {
this.tokenSubject.next(token); // 如果 Local Storage 中有 Token,則存入 State
}
}
// 讓其他組件能夠訂閱 Token 狀態變化
get token$(): Observable<string | null> {
return this.tokenSubject.asObservable();
}
// 處理登入請求並存入 Token
login(username: string, password: string): Observable<any> {
return this.http.post(`${this.apiUrl}/login`, { username, password }).pipe(
tap((response: any) => {
localStorage.setItem('token', response.token); // 將 JWT 存入 Local Storage
this.tokenSubject.next(response.token); // 更新 State
})
);
}
// 登出並清除 Token
logout() {
localStorage.removeItem('token');
this.tokenSubject.next(null); // 清除 State 中的 Token
}
}
現在我們已經在 AuthService
中存入了 JWT 並將它存成 State,接下來我們可以在需要的組件中訂閱這個 State。這樣,當用戶登入或登出時,組件可以自動更新顯示的內容,例如導覽列上顯示的登入或登出按鈕。
app.component.ts
:import { Component, OnInit } from '@angular/core';
import { AuthService } from './services/auth.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
isLoggedIn = false; // 用來判斷使用者是否登入
constructor(private authService: AuthService) {}
ngOnInit(): void {
// 訂閱 AuthService 的 Token State
this.authService.token$.subscribe((token) => {
this.isLoggedIn = !!token; // 如果有 Token,表示已登入
});
}
logout(): void {
this.authService.logout(); // 呼叫登出,清除 Token
}
}
app.component.html
:<mat-toolbar color="primary">
<button mat-button *ngIf="!isLoggedIn" routerLink="/login">登入</button>
<button mat-button *ngIf="isLoggedIn" (click)="logout()" routerLink="/login">
登出
</button>
</mat-toolbar>
<router-outlet></router-outlet>
在這裡,我們使用了 isLoggedIn
來控制登入和登出的按鈕顯示。當用戶登入時,我們會顯示登出按鈕;當用戶未登入時,我們會顯示登入按鈕。
這種做法的好處在於,我們不需要每次從 Local Storage 讀取 JWT,而是將它存成 State,並且讓應用程式的其他部分能夠即時追蹤登入狀態的變化。這對於大型應用來說,能夠提升性能並讓使用者體驗更加順暢。
當用戶登入成功並儲存了 JWT 之後,接下來我們需要確保每次發送 API 請求時,都自動攜帶這個 Token。在 Angular 中,這可以透過 HttpInterceptor 來實現。攔截每次 HTTP 請求,並自動將 JWT 加入到請求的 Authorization Header 中。
auth.interceptor.ts
我們可以透過 Angular 的 HttpInterceptor 來攔截並修改每一個 HTTP 請求,將 JWT Token 加入到請求的標頭中。
// src/app/interceptors/auth.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.getItem('token'); // 從 Local Storage 取得 JWT
if (token) {
const cloned = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`), // 加入 Authorization 標頭
});
return next.handle(cloned); // 攜帶 Token 發送請求
} else {
return next.handle(req); // 如果沒有 Token,直接處理原請求
}
}
}
app.module.ts
中註冊 Interceptor記得將這個 Interceptor 註冊到應用中,以便每次 HTTP 請求都會自動攜帶 JWT Token。
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './services/auth.interceptor';
@NgModule({
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
],
})
export class AppModule {}
在應用中,我們可能會有一些頁面僅限於已登入的用戶訪問,例如個人資料頁面、管理員頁面等。我們可以使用 Route Guard 來保護這些頁面,確保只有已登入的用戶才能存取。
auth.guard.ts
我們將使用 Angular 的 CanActivate 來保護路由。每當用戶試圖訪問受保護的頁面時,Angular 會透過這個 Guard 來檢查用戶是否已經登入並持有有效的 JWT Token。如果未登入,則重導到登入頁面。
src/app/guards/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from '../services/auth.service'; // 請確認路徑與 AuthService 檔案位置一致
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(): boolean {
const token = localStorage.getItem('token'); // 從 Local Storage 取得 JWT
if (token) {
return true; // 已登入,允許進入頁面
} else {
this.router.navigate(['/login']); // 未登入,重導至登入頁面
return false; // 拒絕進入頁面
}
}
}
AuthGuard
接下來,我們需要將這個 Guard 應用到我們想保護的路由上,這樣當用戶未登入時,就會自動重導到登入頁面。
首先,找到你的路由檔案 app-routing.module.ts
,並在其中使用 AuthGuard
來保護路由。
src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { HomeComponent } from './home/home.component';
import { AuthGuard } from './guards/auth.guard';
const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'home', component: HomeComponent, canActivate: [AuthGuard] }, // 保護首頁
{ path: '', redirectTo: '/login', pathMatch: 'full' }, // 預設重定向到 login 頁面
{ path: '**', redirectTo: '/login' }, // 未匹配的路由重定向到 login 頁面
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
AuthGuard
在這裡,我們將 AuthGuard
應用到 /home
路由上。當用戶訪問這些路徑時,系統會先檢查 Local Storage 中是否有 JWT。如果有,則允許進入頁面,否則重導至 /login
。
如果你在訪問受保護的頁面時沒有攜帶 Token,Angular 會自動攔截,並重導使用者到登入頁面,這樣就能確保受保護的頁面不會被未登入的使用者看到。
JWT 身份驗證的核心在於前後端的無縫整合,這裡有幾個重要的細節需要注意。
你可以使用 cors
這個 Node.js 的中介軟體來解決跨域問題。首先,我們需要安裝這個套件。
npm install cors
當前後端分開部署時,可能會遇到 CORS(跨域資源共享) 問題,這會導致前端無法向後端發送請求。為了避免這個問題,我們需要在後端設置 CORS 許可,允許特定的前端域名訪問 API。
在 Express 中,可以使用 cors
套件來解決這個問題:
const cors = require('cors');
app.use(cors({ origin: 'http://localhost:4200' })); // 允許 Angular 本地伺服器發送請求
這篇文章詳細講解了如何在前端實現 JWT 登入驗證,以及前後端如何整合這個流程。從登入畫面的設定、Token 的儲存與管理、到攜帶 JWT 發送 API 請求,我們一步步展示了如何確保用戶的身份安全和 API 的受保護。
透過這樣的流程,你可以在開發過程中建立一個更安全、可擴展的身份驗證系統,讓你的應用程式更加穩定。接下來,你可以根據這些流程,開始實現更多受保護的功能,並針對不同使用者設計專屬的權限控管系統。