在現代 Web 應用架構中,用戶往往需要同時使用多個關聯的業務系統(如電商平台的商品頁、購物車、支付中心)。單點登入(SSO)技術透過「一次登入,多系統通行」的特性,徹底解決了用戶在多系統間重複登入的痛點。本文基於簡單的實際專案案例,詳細介紹 SSO 在多端應用中的設計思路與實現方案,包含整頁重定向與彈窗通信兩種核心模式,並結合 client1、client2、client3 的具體程式碼實現進行說明。
這個專案我採用「三端兩模式」架構(多終端協同 + 認證 - 業務分離),透過統一的登入中心串聯多個業務應用,實現跨應用的身份共享。
業務應用(Client):client1、client3(基於 Vite + Vue Router 構建的前端應用),提供具體業務功能,需依賴登入狀態訪問受保護資源。兩者程式碼結構基本相同,都透過路由守衛和請求攔截器處理登入相關邏輯。
登入中心(Auth Server):client2(獨立前端應用),負責統一身份認證、token 發放與登入狀態管理,是整個 SSO 體系的信任源。
後端服務(Backend):提供 API 介面的後端服務(運行在 localhost:5000),透過校驗 token 合法性控制資源訪問,當檢測到未授權請求(401 錯誤)時觸發登入流程。
SSO 的核心是解決「登入狀態跨應用傳遞」的問題,本專案實現了兩種典型互動模式:
整頁重定向模式:業務應用透過頁面跳轉將用戶引導至登入中心,登入成功後攜帶 token 重定向回原應用,適用於簡單場景或彈窗被攔截時的降級方案。
彈窗 + postMessage 模式:業務應用透過 window.open 彈出登入中心,登入成功後利用 postMessage API 將 token 安全傳遞回主應用,保留用戶操作上下文,是本專案的推薦方案。
彈窗模式透過「主應用 - 彈窗 - 主應用」的通信閉環實現登入狀態傳遞,既保證安全性又優化用戶體驗,核心流程分為三個階段:
當用戶訪問業務應用的受保護資源時,系統透過兩種方式判斷未登入狀態:
請求攔截:API 請求返回 401 錯誤(後端檢測 token 無效或缺失)。
路由守衛:路由跳轉時,前端路由守衛檢測到本地無有效 token。
此時,業務應用透過 window.open 打開登入中心,並攜帶自身 origin(如 http://localhost:5173)作為 resource 參數,用於登入成功後的回調定位。
以 client1 的響應攔截器程式碼為例:
// client1的request.js響應攔截器
request.interceptors.response.use((res) => {
if (res.data.status === 401) {
// 打開登入中心彈窗,攜帶當前應用origin
window.open(`http://localhost:5174/login?resource=${window.location.origin}`);
}
return res;
})
業務應用在初始化時註冊 message 事件監聽,專門接收登入中心透過 postMessage 傳遞的 token 資訊。為防止惡意網站偽造消息,需嚴格校驗發送方的 origin。
client1 在 main.js 中註冊監聽:
// client1的main.js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
window.addEventListener("message",(event) => {
const token = event.data.token;
// 存儲接收到的token
localStorage.setItem("token",token);
// 刷新頁面使token生效
window.location.reload()
})
app.use(router).mount('#app')
登入中心加載時解析 resource 參數(業務應用的 origin),用戶完成登入後,透過 postMessage 將 token 定向回傳至業務應用。為保證安全性,需處理 opener 丟失(如彈窗被刷新)的邊緣情況。
client1 中處理登入成功回傳 token 的相關程式碼:
// client1中登入相關邏輯
import request from "../server/request";
import { useRoute } from "vue-router";
import { watch, ref } from "vue";
const route = useRoute();
const resource = ref("");
const token = localStorage.getItem("token");
function postMessage(token, resource){
window.opener.postMessage({
token:token
},resource.value)
}
watch(
() => route.query.resource,
(val) => {
resource.value = val ? decodeURIComponent(val) : "";
if (token) {
postMessage(token,resource.value)
}
},
{ immediate: true }
);
function login() {
request.get("/login").then((res) => {
const apitoken = res.data.data;
localStorage.setItem("token", apitoken);
// 向打開登入頁的業務應用回傳token
window.opener.postMessage({token:apitoken},resource.value)
// 方案之一:整頁重定向
window.location.href = `${resource.value}?token=${token}`;
// 關閉登入彈窗
window.close()
});
}
業務應用需透過路由守衛和請求攔截器實現 token 的自動管理,確保登入狀態在路由跳轉和 API 請求中無縫生效。
路由守衛負責在頁面跳轉時校驗登入狀態,對未登入用戶攔截並觸發登入流程,同時處理整頁重定向模式下的 ?token= 參數。
client1 的路由守衛程式碼:
// client1的router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomePage from '../pages/HomePage.vue'
import AboutPage from '../pages/AboutPage.vue'
const routes = [
{ path: '/', component: HomePage },
{ path: '/about', component: AboutPage },
]
const router = createRouter({
history: createWebHistory(),
routes,
})
router.beforeEach((to, from, next) => {
// 處理整頁重定向帶回的token
const token = to.query.token;
if (token) {
localStorage.setItem("token", token);
}
next();
})
export default router
透過 Axios 攔截器實現 token 的自動攜帶,以及 401 錯誤的統一處理,確保 API 請求的安全性。
client1 的請求攔截器程式碼:
// client1的request.js請求攔截器
request.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
config.headers = config.headers || {}
if (token) config.headers.token = token
return config
})
client1 作為主要的業務應用,實現了完整的 SSO 邏輯:
client2 作為登入中心,提供登入頁面和登入功能:主要是為了獲取 token
<template>
<div>
<div>帳號:<input /></div>
<div>密碼:<input /></div>
<div>
<button
@click="() => login()"
>
登入
</button>
</div>
</div>
</template>
其請求攔截器與 client1 類似,但登入頁一般不做 401 重定向,避免循環跳轉:
// client2的request.js
request.interceptors.response.use((res) => {
if (res.data.status === 401) {
window.open(`http://localhost:5174/login?resource=${window.location.origin}`)
}
return res;
})
client3 與 client1 程式碼結構基本相同,作為另一個業務應用,驗證 SSO 的跨應用登入效果:
<script setup>
import request from '../server/request'
// 觸發一次請求,用於檢測登入狀態
request.get('/api1').catch(() => {})
</script>
<template>
<main>
<h1>Client3 首頁(SSO 演示)</h1>
<p>如果已在其他專案登入,這裡將直接顯示;否則會被重定向到登入頁。</p>
<router-link to="/">Home</router-link>
<router-view></router-view>
</main>
</template>
單點登入的實現涉及多端互動和安全校驗,以下細節直接影響方案的穩定性和安全性:
必須使用業務應用的 origin(如 http://localhost:5173)作為 targetOrigin,禁止使用 *(允許任意域名接收),否則可能導致 token 被惡意網站竊取。
僅當登入中心透過 window.open 打開時,opener 才指向業務應用窗口;若用戶刷新登入彈窗,opener 會變為 null,需降級為整頁重定向,如 client1 的 login 函數中同時實現了 postMessage 和整頁重定向。
業務應用傳遞 origin 時需用 encodeURIComponent 編碼(處理特殊字符),登入中心接收後用 decodeURIComponent 解碼,避免 URL 解析錯誤,如 client1 中 watch 監聽 resource 參數時進行了解碼處理。
無論是否允許路由跳轉,beforeEach 守衛都必須調用 next(),否則會導致頁面卡死(不渲染),client1 的路由守衛中正確實現了這一點。
若瀏覽器因「非用戶主動觸發」攔截彈窗(如自動執行的 window.open),需捕獲錯誤並降級為整頁重定向。
模式 | 優勢 | 劣勢 | 适用场景 |
---|---|---|---|
彈窗 + postMessage | 不刷新頁面,保留用戶操作上下文;體驗流暢 | 需處理彈窗攔截、opener 丟失等邊緣情況 | 主流場景:用戶主動觸發的登入(如點擊「我的」按鈕) |
整頁重定向 | 實現簡單,無瀏覽器兼容性問題 | 刷新頁面,丟失當前操作狀態 | 降級方案:彈窗被攔截時;簡單應用或舊瀏覽器 |
推薦實現「智能兼容」邏輯:登入中心優先嘗試 postMessage 回傳,檢測到 opener 無效時自動切換為整頁重定向,確保所有場景下的可用性,client1 的登入函數已實現此邏輯。
單點登入(SSO)的核心是透過「統一認證 + 跨域通信」實現多應用的身份共享。本專案透過「彈窗 + postMessage」模式優化用戶體驗,同時以整頁重定向作為兜底,兼顧了安全性與兼容性。
關鍵成功要素包括:
透過這套示例,業務應用(client1、client3)無需關心登入邏輯,只需專注自身業務;登入中心(client2)統一處理身份認證,實現「一次登入,全平台通行」的目標,為用戶提供無縫的跨應用體驗。實際應用中,可根據具體業務場景對這套程式碼進行擴展和優化,如增加 token 過期刷新機制、完善錯誤處理等。
如果您覺得這篇文章對您有幫助,歡迎點讚和收藏,大家的支持是我繼續創作優質內容的動力🌹🌹🌹也希望您能在😉😉😉我的首頁 😉😉😉找到更多對您有幫助的內容。