单点登录(SSO)在多端应用中的设计与实现
在现代 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弹出登录中心,登录成功后利用postMessageAPI 将 token 安全传递回主应用,保留用户操作上下文,是本项目的推荐方案。
二、核心登录流程(弹窗 + postMessage 模式)
弹窗模式通过 "主应用 - 弹窗 - 主应用" 的通信闭环实现登录状态传递,既保证安全性又优化用户体验,核心流程分为三个阶段:
1. 触发登录:业务应用检测未登录状态
当用户访问业务应用的受保护资源时,系统通过两种方式判断未登录状态:
- 请求拦截:API 请求返回 401 错误(后端检测 token 无效或缺失)。
- 路由守卫:路由跳转时,前端路由守卫检测到本地无有效 token。
此时,业务应用通过window.open打开登录中心,并携带自身origin(如http://localhost:5173)作为resource参数,用于登录成功后的回调定位。
以 client1 的响应拦截器代码为例:
js
// 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;
})
2. 监听回调:业务应用准备接收 token
业务应用在初始化时注册message事件监听,专门接收登录中心通过postMessage传递的 token 信息。为防止恶意网站伪造消息,需严格校验发送方的origin。
client1 在 main.js 中注册监听:
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')
3. 回传 token:登录中心完成认证并响应
登录中心加载时解析resource参数(业务应用的origin),用户完成登录后,通过postMessage将 token 定向回传至业务应用。为保证安全性,需处理opener丢失(如弹窗被刷新)的边缘情况。
client1 中处理登录成功回传 token 的相关代码:
js
// 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 请求中无缝生效。
1. 路由守卫:控制页面访问权限
路由守卫负责在页面跳转时校验登录状态,对未登录用户拦截并触发登录流程,同时处理整页重定向模式下的?token=参数。
client1 的路由守卫代码:
js
// 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
2. 请求拦截器:自动携带与刷新 token
通过 Axios 拦截器实现 token 的自动携带,以及 401 错误的统一处理,确保 API 请求的安全性。
client1 的请求拦截器代码:
js
// 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
})
四、各端代码说明
1. client1 代码特点
client1 作为主要的业务应用,实现了完整的 SSO 逻辑:
- 请求拦截器自动携带 token
- 响应拦截器在 401 时打开登录中心弹窗
- 路由守卫处理整页重定向带来的 token
- 注册 message 事件监听接收登录中心回传的 token
- 实现了通过 postMessage 向业务应用回传 token 的功能
2. client2(登录中心)代码特点
client2 作为登录中心,提供登录页面和登录功能:主要是为了获取token
vue
<template>
<div>
<div>账号名:<input /></div>
<div>密码:<input /></div>
<div>
<button
@click="() => login()"
>
登录
</button>
</div>
</div>
</template>
其请求拦截器与 client1 类似,但登录页一般不做 401 重定向,避免循环跳转:
js
// 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;
})
3. client3 代码特点
client3 与 client1 代码结构基本相同,作为另一个业务应用,验证 SSO 的跨应用登录效果:
vue
<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>
五、关键细节与避坑指南
单点登录的实现涉及多端交互和安全校验,以下细节直接影响方案的稳定性和安全性:
- postMessage 的 targetOrigin 校验
必须使用业务应用的origin(如http://localhost:5173)作为targetOrigin,禁止使用*(允许任意域名接收),否则可能导致 token 被恶意网站窃取。
- window.opener 的可用性处理
仅当登录中心通过window.open打开时,opener才指向业务应用窗口;若用户刷新登录弹窗,opener会变为null,需降级为整页重定向,如 client1 的 login 函数中同时实现了 postMessage 和整页重定向。
- resource 参数的编解码
业务应用传递origin时需用encodeURIComponent编码(处理特殊字符),登录中心接收后用decodeURIComponent解码,避免 URL 解析错误,如 client1 中 watch 监听 resource 参数时进行了解码处理。
- 路由守卫必须调用 next()
无论是否允许路由跳转,beforeEach守卫都必须调用next(),否则会导致页面卡死(不渲染),client1 的路由守卫中正确实现了这一点。
- 弹窗被浏览器拦截的兼容
若浏览器因 "非用户主动触发" 拦截弹窗(如自动执行的window.open),需捕获错误并降级为整页重定向。
六、两种模式的选用场景
| 模式 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| 弹窗 + postMessage | 不刷新页面,保留用户操作上下文;体验流畅 | 需处理弹窗拦截、opener丢失等边缘情况 | 主流场景:用户主动触发的登录(如点击 "我的" 按钮) |
| 整页重定向 | 实现简单,无浏览器兼容性问题 | 刷新页面,丢失当前操作状态 | 降级方案:弹窗被拦截时;简单应用或旧浏览器 |
推荐实现 "智能兼容" 逻辑:登录中心优先尝试postMessage回传,检测到opener无效时自动切换为整页重定向,确保所有场景下的可用性,client1 的登录函数已实现此逻辑。
七、总结
单点登录(SSO)的核心是通过 "统一认证 + 跨域通信" 实现多应用的身份共享。本项目通过 "弹窗 + postMessage" 模式优化用户体验,同时以整页重定向作为兜底,兼顾了安全性与兼容性。
关键成功要素包括:
- 严格的origin校验,防止 token 跨域泄露;
- 完善的边缘情况处理(如opener丢失、弹窗拦截);
- 统一的 token 管理机制(路由守卫 + 请求拦截器)。
通过这套示例,业务应用(client1、client3)无需关心登录逻辑,只需专注自身业务;登录中心(client2)统一处理身份认证,实现 "一次登录,全平台通行" 的目标,为用户提供无缝的跨应用体验。实际应用中,可根据具体业务场景对这套代码进行扩展和优化,如增加 token 过期刷新机制、完善错误处理等。
如果您觉得这篇文章对您有帮助,欢迎点赞和收藏,大家的支持是我继续创作优质内容的动力🌹🌹🌹也希望您能在😉😉😉我的主页 😉😉😉找到更多对您有帮助的内容。
- 致敬每一位赶路人