在前兩天的修煉中,我們成功地引導使用者完成了 Google 的授權流程。現在,該如何將這個授權,轉化為我們系統內部的登入狀態?
今天,我們將學習如何從前端取得使用者身份憑證,並與後端配合,打造一套安全、可靠的登入機制。
我們的核心策略是:前端負責快速取得 Google 的 ID Token
,然後立即將其交給後端,由後端完成所有安全性驗證工作。
首先,我們使用 Google Identity Services (GSI) 函式庫來渲染登入按鈕,並在使用者成功登入後,於回呼函式中取得 ID Token
。
1. 在 HTML 中準備一個渲染按鈕的容器
<div id="googleLogin"></div>
2. 在 Vue 元件中初始化並渲染按鈕
import { onMounted } from 'vue';
const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
// 在 setup 中
onMounted(() => {
if (window.google) {
window.google.accounts.id.initialize({
client_id: clientId,
callback: handleCredentialResponse, // 登入成功後的回呼函式
});
window.google.accounts.id.renderButton(
document.getElementById('buttonDiv'), // 渲染按鈕的容器
{ theme: 'outline', size: 'large' }, // 按鈕樣式設定
);
}
});
function handleCredentialResponse(response) {
// 將這個 Token 發送到後端
sendTokenToBackend(response.credential);
}
前端的職責很單純:拿到 response.credential
後,立刻透過 API 請求將它發送到後端進行驗證。
// 前端發送請求
async function sendTokenToBackend(googleIdToken) {
const backendResponse = await fetch('/api/auth/google-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: googleIdToken }),
});
if (backendResponse.ok) {
const { appToken } = await backendResponse.json();
// 成功!拿到我們自家系統的 Token
// 接下來就是儲存這個 appToken 的問題了
}
}
這是整個流程中最關鍵的安全環節。後端收到前端傳來的 googleIdToken
後:
aud
(Audience):必須是我們在 Google Cloud Console 申請的那個 Client ID。iss
(Issuer):必須是 https://accounts.google.com
或 accounts.google.com
。exp
(Expiration Time):必須還沒過期。各大後端語言通常都有官方或社群維護的函式庫(例如 Node.js 的 google-auth-library
)可以輕鬆完成這項驗證工作。
參考:Google doc - 驗證伺服器端的 Google ID 權杖
一旦後端確認 Google ID Token 是真實有效的,就等於確認了使用者的合法身份。此時,後端會為這位使用者簽發一個屬於我們自己應用程式的 Token(我們自己的 JWT),並將其回傳給前端。
這個「自家 Token」才是前端後續用來證明「我在 POS 系統中已登入」的唯一憑證。
當後端驗證成功並回傳我們自家的 appToken
後,下一個關鍵問題便是:前端該如何安全地儲存這個 Token?
你可能會問,身為一個 Vue 開發者,是否有專屬於 Vue 的儲存方法?這是一個很好的問題!答案是:Token 的儲存策略是瀏覽器層級的 Web 標準,而非框架層級的。 無論使用 Vue、React 還是 Angular,我們都得在瀏覽器提供的幾種機制中做選擇。
localStorage
/ sessionStorage
localStorage
可以在關閉瀏覽器後依然保存。localStorage.setItem('token', a_token)
。// store/auth.js (Pinia)
actions: {
setToken(token) {
this.token = token;
localStorage.setItem('token', token);
},
clearToken() {
this.token = null;
localStorage.removeItem('token');
}
}
說明:將 Token 存放在一個 JavaScript 變數中。
優點:相對安全,不會被 XSS 輕易竊取。
缺點:頁面一重新整理,Token 就會消失,使用者需要重新登入,體驗較差。
Vue 實作方式:
// store/auth.js (Pinia)
import { defineStore } from 'pinia';
export const useAuthStore = defineStore('auth', {
state: () => ({
token: null,
user: null,
}),
actions: {
setToken(token) {
this.token = token;
// 你可以在這裡解析 token 獲取 user info
},
},
});
HttpOnly
Cookie (推薦)Set-Cookie
標頭將 Token 寫入瀏覽器的 Cookie 中,並設定 HttpOnly
屬性。SameSite
屬性緩解)。axios
等工具發送跨來源 (Cross-origin) 請求時,需要設定 withCredentials: true
,瀏覽器才會願意把 Cookie 一起帶上。// axios 設定
const apiClient = axios.create({
baseURL: 'https://api.your-domain.com',
withCredentials: true, // 允許跨來源請求攜帶 Cookie
});
即使在 Vue 3 中,我們依然是圍繞著這幾種瀏覽器標準來進行 Token 管理。
HttpOnly
Cookie。這是業界公認的最佳實踐,雖然需要後端配合,但一勞永逸。localStorage
。當我們需要請求受保護的後端資源時(例如:獲取訂單列表),就必須在請求中帶上我們的通行證,以證明身份。
如果使用 HttpOnly
Cookie:瀏覽器會自動處理。
如果使用 localStorage
或記憶體:你需要手動在 HTTP 請求的標頭 (Header) 中加入 Authorization
欄位。
通常我們會使用 axios
這類的 HTTP 客戶端,並透過「攔截器 (Interceptor)」來統一處理。
// 使用 axios 攔截器
import axios from 'axios';
import { store } from './store'; // 假設是 Pinia store
const apiClient = axios.create({
baseURL: '/api',
});
// 請求攔截器
apiClient.interceptors.request.use((config) => {
const token = store.state.token; // 從記憶體獲取 token
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 後續所有透過 apiClient 發出的請求,都會自動帶上 Token
export default apiClient;
今天,我們完成了登入流程的最後一哩路,並專注於「隱含式流程」的實現。我們確立的完整流程是:
前端取得 Google ID Token
-> 後端驗證 Google ID Token
-> 後端簽發自家 JWT
-> 前端儲存 JWT
-> 後續請求攜帶 JWT
我們理解到,為了安全性,向後端發送憑證並進行二次驗證的步驟也絕對不可省略。同時,我們也分析了前端儲存 Token 的各種策略,並強烈推薦使用 HttpOnly
Cookie 作為最安全的方案。
至此,「Authの呼吸」的前三個型態已經讓我們擁有了一套清晰且安全的驗證機制。
明日,Day 18:[Routerの呼吸・壹之型] 導航系統 - 理解SPA路由機制。心を燃やせ 🔥!