iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0
Vue.js

打造銷售系統30天修練 - 全集中・Vue之呼吸系列 第 16

Day 16:[Authの呼吸・貳之型] SSO整合 - Google OAuth登入實作(下)

  • 分享至 

  • xImage
  •  

在上篇中,我們已經完成了所有理論學習和前置作業,並成功取得了 Google OAuth 的 Client ID。

現在,是時候將這些準備工作付諸實踐了。本篇將深入 LoginView.vue<script setup> 區塊,逐行解析我們是如何利用 Google Identity Services 函式庫來觸發登入流程,並取得授權碼的。

程式碼結構

讓我們先看看 LoginView.vue 中的主要程式碼結構:

//常數配置
const STORE_OPTIONS = [
  { value: '', text: '請選擇您的門市...', disabled: true },
  { value: 'test1', text: '測試門市1' },
  { value: 'test2', text: '測試門市2' },
  { value: 'test3', text: '測試門市3' },
  { value: 'test4', text: '測試門市4' },
  { value: 'test5', text: '測試門市5' },
  { value: 'test6', text: '測試門市6' },
];

const ERROR_MESSAGES = {
  STORE_REQUIRED: '請選擇您的門市',
  CONFIG_ERROR: '系統配置錯誤,請聯繫系統管理員',
  GOOGLE_LOAD_FAILED: 'Google 服務加載失敗,請稍後再試',
  LOGIN_FAILED: '登入失敗,請重試',
  LOGIN_ERROR: '登入過程中發生錯誤,請重試',
  BACKEND_ERROR: '後端驗證失敗,請重試',
  NETWORK_ERROR: '網路錯誤,請重試',
};

// 環境變數
const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;

googleLogin 函式深度解析

現在讓我們來看核心函式 googleLogin

async function googleLogin() {
  // 1. 前置檢查
  if (!validateStore()) {
    return;
  }

  if (!isClientIdConfigured()) {
    storeError.value = ERROR_MESSAGES.CONFIG_ERROR;
    return;
  }

  if (!isGoogleServiceAvailable()) {
    storeError.value = ERROR_MESSAGES.GOOGLE_LOAD_FAILED;
    return;
  }

  isLoading.value = true;
  clearError();

  try {
    // 2. 初始化 Google Client
    const oauth2 = window.google.accounts.oauth2.initCodeClient({
      client_id: clientId,
      scope: 'openid email profile',
      ux_mode: 'popup',
      redirect_uri: window.location.origin,
      callback: handleOAuthCallback,
    });

    // 3. 觸發登入視窗
    oauth2.requestCode();
  } catch (error) {
    handleLoginError(error);
  }
}

// 輔助函式
function validateStore() {
  if (!storeSelect.value) {
    storeError.value = ERROR_MESSAGES.STORE_REQUIRED;
    return false;
  }
  storeError.value = '';
  return true;
}

function clearError() {
  storeError.value = '';
}

function isGoogleServiceAvailable() {
  return window.google?.accounts?.oauth2;
}

function isClientIdConfigured() {
  return !!clientId;
}

async function handleOAuthCallback(response) {
  isLoading.value = false;

  if (response.error) {
    storeError.value = ERROR_MESSAGES.LOGIN_FAILED;
    return;
  }

  // 必須成功與後端驗證才能登入
  try {
    const backendResponse = await fetch('/api/auth/google', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        code: response.code,
        store: storeSelect.value,
      }),
    });

    if (backendResponse.ok) {
      const { token, user } = await backendResponse.json();
      // 後端驗證成功,保存 JWT token 和用戶資訊
      localStorage.setItem('authToken', token);
      if (user) {
        localStorage.setItem('userInfo', JSON.stringify(user));
      }
      // 登入成功,導向到指定頁面
      router.push('/dashboard');
    } else {
      const errorData = await backendResponse.json();
      storeError.value = errorData.message || ERROR_MESSAGES.BACKEND_ERROR;
    }
  } catch (error) {
    console.error('Backend authentication error:', error);
    storeError.value = ERROR_MESSAGES.NETWORK_ERROR;
  }
}

function handleLoginError(error) {
  isLoading.value = false;
  storeError.value = ERROR_MESSAGES.LOGIN_ERROR;
  console.error('Google login error:', error);
}

1. 前置檢查 (Pre-flight Checks)

在執行主要邏輯前,我們進行了三項重要的檢查:

  • validateStore():確保使用者已經選擇了門市。
  • isClientIdConfigured():檢查 .env 檔案中的 Client ID 是否成功讀取。如果沒有,可能是環境變數設定有誤。
  • isGoogleServiceAvailable():檢查從 index.html 引入的 Google GSI 函式庫是否已成功載入並掛載到 window 物件上。

2. 初始化 Google Client (initCodeClient)

這是整個流程的核心。window.google.accounts.oauth2.initCodeClient 會建立並初始化一個用於「授權碼流程」的客戶端。我們傳入一個設定物件:

  • client_id: 我們在上篇中取得並設定在環境變數中的用戶端 ID。
  • scope: 我們向 Google 請求的權限範圍。
    • openid: 用於 OpenID Connect 流程,是驗證使用者身份的標準。
    • email: 請求存取使用者的電子郵件地址。
    • profile: 請求存取使用者的基本公開資料(姓名、頭像等)。
  • ux_mode: 使用者體驗模式。我們選擇 'popup',這會彈出一個新的登入視窗,體驗較佳。另一個選項是 'redirect',會將整個頁面重新導向到 Google。
  • redirect_uri: 重新導向 URI。雖然在 popup 模式下它不是那麼顯著,但仍是安全驗證的必要一環,必須與 Cloud Console 中的設定相符。我們使用 window.location.origin 來動態獲取當前的來源網址。
  • callback: 回呼函式。這是最關鍵的部分,當使用者在彈出視窗中完成登入與授權後,Google 會觸發這個函式。

3. 觸發登入視窗 (requestCode)

initCodeClient 只是做好了準備工作,oauth2.requestCode() 才是真正執行「向使用者請求授權碼」這個動作的命令。執行後,就會跳出我們熟悉的 Google 登入彈窗。

4. 處理回呼 (Handling the Callback)

當使用者完成操作後,我們在 handleOAuthCallback 函式中處理回呼邏輯:

  • isLoading.value = false: 無論成功或失敗,非同步操作已結束,我們首先要做的就是關閉 Loading 狀態。
  • response.error: Google 會透過 response 物件告訴我們結果。如果 response.error 存在,表示使用者拒絕授權或發生了其他錯誤。
  • response.code: 如果一切順利,response 物件中就會包含我們夢寐以求的「授權碼 (Authorization Code)」!

網域限制:我們在 Google Cloud Console 中設定了網域限制,只允許 @XXX.com 結尾的 email 登入。這樣 Google 會在驗證階段就過濾掉不符合條件的用戶,確保只有授權的員工才能登入系統。

授權碼的安全處理

我們取得的 response.code 是「授權碼 (Authorization Code)」,這是 OAuth 流程的第一步。為了確保安全性,我們立即將授權碼送到後端進行驗證和交換。

為什麼需要後端處理?

  1. 安全性考量:授權碼包含敏感信息,不應該在前端直接使用或保存
  2. OAuth 標準流程:授權碼必須由後端向 Google 換取真正的 access token
  3. 防止攻擊:後端驗證可以防止 CSRF 等安全攻擊
  4. Token 管理:後端可以安全地管理 JWT token 的生成和驗證

我們的實作方式

handleOAuthCallback 函式中,我們實作了完整的安全流程:

  1. 網域驗證:Google 已經驗證了用戶的 email 網域,確保只有 @XXX.com 的用戶可以登入
  2. 立即發送授權碼:取得授權碼後,立即發送到後端 API
  3. 後端驗證:後端向 Google 換取 access token 並生成 JWT
  4. Token 保存:前端保存後端回傳的 JWT token 和用戶資訊
  5. 自動導向:登入成功後自動導向到 dashboard

這樣的實作方式結合了 Google 的網域限制和 OAuth 2.0 的安全標準,既確保了安全性,又提供了良好的用戶體驗。

總結

恭喜!我們已經成功地將 Google OAuth 登入整合到了我們的 Vue 應用中。透過上下兩篇的修煉,我們不僅學會了如何在 Google Cloud Console 進行繁瑣的設定,也深度理解了如何在 Vue 中呼叫 Google GSI 函式庫,並實作了完整的「授權碼」安全處理流程。

「Authの呼吸・壹之型」讓我們掌握了前端在 SSO 流程中的關鍵職責,並確保了整個登入流程的安全性。這已經為我們的 POS 系統打開了通往安全、便捷驗證的大門。

明日,Day 17:[Authの呼吸・參之型] 登入流程 - Token管理與安全性。心を燃やせ 🔥!


上一篇
Day 15:[Authの呼吸・壹之型] SSO整合 - Google OAuth登入實作(上)
下一篇
Day 17:[Authの呼吸・參之型] 登入流程 - Token管理與安全性
系列文
打造銷售系統30天修練 - 全集中・Vue之呼吸19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言