多端单点登录(SSO)实战:从架构设计到代码实现

单点登录(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 的核心是解决 "登录状态跨应用传递" 的问题,本项目实现了两种典型交互模式:

  1. 整页重定向模式:业务应用通过页面跳转将用户引导至登录中心,登录成功后携带 token 重定向回原应用,适用于简单场景或弹窗被拦截时的降级方案。
  1. 弹窗 + 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>

五、关键细节与避坑指南

单点登录的实现涉及多端交互和安全校验,以下细节直接影响方案的稳定性和安全性:

  1. postMessage targetOrigin 校验

必须使用业务应用的origin(如http://localhost:5173)作为targetOrigin,禁止使用*(允许任意域名接收),否则可能导致 token 被恶意网站窃取。

  1. window.opener 的可用性处理

仅当登录中心通过window.open打开时,opener才指向业务应用窗口;若用户刷新登录弹窗,opener会变为null,需降级为整页重定向,如 client1 的 login 函数中同时实现了 postMessage 和整页重定向。

  1. resource 参数的编解码

业务应用传递origin时需用encodeURIComponent编码(处理特殊字符),登录中心接收后用decodeURIComponent解码,避免 URL 解析错误,如 client1 中 watch 监听 resource 参数时进行了解码处理。

  1. 路由守卫必须调用 next()

无论是否允许路由跳转,beforeEach守卫都必须调用next(),否则会导致页面卡死(不渲染),client1 的路由守卫中正确实现了这一点。

  1. 弹窗被浏览器拦截的兼容

若浏览器因 "非用户主动触发" 拦截弹窗(如自动执行的window.open),需捕获错误并降级为整页重定向。

六、两种模式的选用场景

模式 优势 劣势 适用场景
弹窗 + postMessage 不刷新页面,保留用户操作上下文;体验流畅 需处理弹窗拦截、opener丢失等边缘情况 主流场景:用户主动触发的登录(如点击 "我的" 按钮)
整页重定向 实现简单,无浏览器兼容性问题 刷新页面,丢失当前操作状态 降级方案:弹窗被拦截时;简单应用或旧浏览器

推荐实现 "智能兼容" 逻辑:登录中心优先尝试postMessage回传,检测到opener无效时自动切换为整页重定向,确保所有场景下的可用性,client1 的登录函数已实现此逻辑。

七、总结

单点登录(SSO)的核心是通过 "统一认证 + 跨域通信" 实现多应用的身份共享。本项目通过 "弹窗 + postMessage" 模式优化用户体验,同时以整页重定向作为兜底,兼顾了安全性与兼容性。

关键成功要素包括:

  • 严格的origin校验,防止 token 跨域泄露;
  • 完善的边缘情况处理(如opener丢失、弹窗拦截);
  • 统一的 token 管理机制(路由守卫 + 请求拦截器)。

通过这套示例,业务应用(client1、client3)无需关心登录逻辑,只需专注自身业务;登录中心(client2)统一处理身份认证,实现 "一次登录,全平台通行" 的目标,为用户提供无缝的跨应用体验。实际应用中,可根据具体业务场景对这套代码进行扩展和优化,如增加 token 过期刷新机制、完善错误处理等。


如果您觉得这篇文章对您有帮助,欢迎点赞和收藏,大家的支持是我继续创作优质内容的动力🌹🌹🌹也希望您能在😉😉😉我的主页 😉😉😉找到更多对您有帮助的内容。

  • 致敬每一位赶路人
相关推荐
小小工匠13 分钟前
LLM - 从定制化 Agent 到 Universal Agent + Skills Library:下一代智能体架构实践
架构·定制化agent·universal agent·skill library
Hilaku19 分钟前
我用 Gemini 3 Pro 手搓了一个并发邮件群发神器(附源码)
前端·javascript·github
IT_陈寒20 分钟前
Java性能调优实战:5个被低估却提升30%效率的JVM参数
前端·人工智能·后端
快手技术21 分钟前
AAAI 2026|全面发力!快手斩获 3 篇 Oral,12 篇论文入选!
前端·后端·算法
颜酱23 分钟前
前端算法必备:滑动窗口从入门到很熟练(最长/最短/计数三大类型)
前端·后端·算法
全栈前端老曹31 分钟前
【包管理】npm init 项目名后底层发生了什么的完整逻辑
前端·javascript·npm·node.js·json·包管理·底层原理
HHHHHY37 分钟前
mathjs简单实现一个数学计算公式及校验组件
前端·javascript·vue.js
boooooooom40 分钟前
Vue3 provide/inject 跨层级通信:最佳实践与避坑指南
前端·vue.js
一颗烂土豆40 分钟前
Vue 3 + Three.js 打造轻量级 3D 图表库 —— chart3
前端·vue.js·数据可视化
青莲84341 分钟前
Android 动画机制完整详解
android·前端·面试