后台太多记不住?我做了一个统一门户把所有系统全串起来了

业务背景

大家好 我是卡卡,在我之前公司的时候,很多业务都需要做私有化部署,每个业务基本上都会有自己的后台,而且还要区分测试后台、正式后台、日志监控、运维工具、发布系统之类的。时间长了,各种子系统越建越多,部署的时候也全是靠我们自己手动搞,所以后期基本就是一堆系统散落在不同服务器上。

特别是我负责项目多的时候,手上要维护的后台实在太多了。用的时候只能把常用的先固定在浏览器标签栏里,但只要换个设备、重装个浏览器,或者清理一下缓存,这些标签全没了,结果又得去聊天记录、文档里一条条翻,才能把后台入口找回来。

更烦的是老板。这个大忙人明明后台地址他自己可以收藏一下,但从来不存,每次要看点数据就来问我要后台地址和账号密码。问得多了我自己都烦了。

所以我后来抽空把这些零散的后台统一起来,做了一个"统一门户管理中心"。所有子系统的入口都放在一个页面里,登录一次之后,用户就能进入自己有权限的所有后台,不用再满世界找链接,也不用每个系统都重新登录。对老板、对运营、对我们开发来说,都省事很多,对后期扩系统的管理也轻松不少。


设计思路

在开始设计这套东西之前,我首先想到的是用单点登录(SSO)机制。传统 SSO 的流程是这样的:用户访问 A 系统,发现没登录,被重定向到认证中心;登录成功后再带着 ticket/code 回来。之后用户访问 B 系统时,因为已经登录过认证中心,就不需要再登录一遍。

这个机制本身没问题,但和我想做的东西不完全一样。传统 SSO 是"先访问系统,再登录"。而我们的内部后台更多是"先登录门户,再从门户进去"。用户不会一上来就直接访问某个子系统,而是先打开统一入口,然后从入口进入对应的后台。

所以我就在想,既然我们的使用场景不是"从子系统开始",那完全照搬传统 SSO 的那套流程,其实就有点不对路。传统 SSO 更像是面向对外的业务系统,比如用户是直接访问 A、B、C 这些系统的,需要 SSO 去帮他们统一一次登录。但我们这种内部后台恰好相反------大家习惯是先打开一个入口页面,然后从这个入口去点各个后台,这一点和传统 SSO 的默认前提完全不一样。

同时我们公司里面的后台系统挺多,而且技术栈也不统一。有 Java 的、有 PHP 的、有 Go 的,还有一些是第三方的自带后台。要让这些系统全部按照 SSO 的协议来改造一遍,不但成本高,而且每个系统都要对接 SSO 的重定向、ticket 验证、登录态同步,工作量特别大,也容易出问题。

所以我最后的思路是:不用大厂那套很重的 SSO,也不用让所有子系统都去支持单点登录,而是做一个轻量一点、贴合我们内部使用习惯的方式。既然大家是从门户进的,那就干脆以门户为中心,把登录这件事情都集中放在门户完成;进入子系统的时候再给子系统一段临时授权码,用这段授权码去认证中心换用户信息,然后子系统再生成自己的 token 就好了。

这样设计我觉得有这样几个好处:

  • 对用户来说还是单点登录体验,登录一次就够了
  • 子系统不用改造太多,不需要支持完整的 SSO 协议
  • 门户只负责入口,认证中心只负责验证,两者职责划分很清楚
  • 不同技术栈的系统都能接入,成本比传统 SSO 低很多

说白了,就是把"统一入口 + 简化授权"的方式结合起来,比传统 SSO 更轻,更适合我们这种内部私有化部署、多后台的场景。


方案选择

在确定整体方向之前,我参考过好几种常见的授权模式。毕竟我们内部的后台系统数量多、技术栈也不统一,Java、PHP、Go 都有,而且部署环境分散,想做统一登录的话,前期选哪种方案基本上决定了后面接入会不会痛苦。

最开始想的是"要不要直接统一 token?"。也就是说,门户登录之后生成一个 JWT,然后所有子系统都用同一个 token 验证。这个做法看起来简单粗暴,但问题也很明显:所有子系统必须共享同一套密钥,一旦某个子系统泄露密钥,所有后台的登录安全就全完了。而且不同系统可能对 token 的字段、时效、校验逻辑都有自己的需求,统一 token 反而会增加耦合。

后来也想过"要不要走 OAuth2 授权码模式?"。这种是大厂常用的做法,流程很标准,但也太重了。要让每个子系统都遵循 OAuth2 的协议、处理 redirect、处理 state、处理 token 交换等等,对我们这种内部后台来说有点过度设计了,而且成本高、实现复杂,不太现实。

再往后就是传统的单点登录(SSO 或 CAS)那类方案。但前面也说了,那一套是"从子系统出发"的:系统 A → 认证中心 → 回 A,再访问 B 又是另一套流程。我们的使用路径是从门户进入,而不是从某个子系统开始,这种场景下传统的 SSO 流程套上去并不贴合,还会让子系统改造量特别大。

综合对比下来,最终我选择了"统一门户 + 一次性 code"的组合方式:

门户负责展示所有系统入口,用户先在门户登录;进入子系统时由门户生成一个一次性 code,子系统拿 code 去认证中心换用户信息,然后再在本地生成自己的 token。

这个模式兼顾了安全性、扩展性和接入成本,也不依赖某种技术栈,所有语言的后台都能很轻松接入,非常适合我们这种内部私有化、多后台的环境。

整个流程大概是这样的:

用户不会直接进入某个子系统,而是先打开统一门户并在门户登录。登录成功后,门户会把用户有权限的后台系统都展示出来。用户点击某个系统入口时,门户会向认证中心申请一个一次性 code,然后带着这个 code 把用户重定向到对应的子系统。

子系统拿到 code 之后,会再去找认证中心验证这个 code 并换取用户信息。拿到 userInfo 后,子系统自己在本地生成一个 token,后面用户在这个子系统里所有的请求,都会通过这个本地 token 来做校验,而不再依赖门户。

这种模式的好处是比较明显的:用户体验上等同于单点登录,登录一次即可进入多个后台;实现上又比传统 SSO 要轻得多,不需要每个子系统都对接完整的重定向流程,也不用统一 token 格式,更适合内部系统多、语言杂、环境分散的情况。


后台设计思路

在搭这个统一门户之前,我先想清楚一个问题:既然它要承载公司所有内部后台,那它本身必须也得是一个标准的后台系统,有自己的模块、有自己的管理逻辑,不能只是把链接拼一拼就完了。

所以我给它拆了几个核心板块,每个板块都是围绕我们内部真实的使用场景来的。

1. 主页(系统入口总览)

这个门户的核心就是"把所有内部系统展示出来"。同事登录之后,首页看到的就是自己有权限访问的所有子系统,按业务分类排好。想进哪个后台,点一下就能跳过去,不用再到处找链接、记书签。

2. 用户管理

所有公司的员工账号都在这里统一管理,不再由各个系统自己造一套。新同事入职的时候,从这个门户注册或录入一次账户,后面所有子系统都能通过它来识别用户,不用每个后台再各自维护一份账号体系。

3. 系统管理(子系统注册)

公司内部每多一个新的私有化服务,比如日志后台、监控平台、部署系统,都要在这里注册一遍。注册之后门户才能展示入口,才能给用户授权,也方便我们后面做跳转、做健康检查。

4. 系统健康检查

既然门户把所有子系统都集中展示了,那系统是否在线、地址是否失效、是否宕机,这些都得有地方看。所以我加了健康检查模块,定时去子系统的 /health 或指定心跳接口探活,一旦某个系统挂了可以第一时间看到,避免同事点进去才发现打不开。

5. 用户授权(哪个员工能进哪些后台)

每个同事不可能看到全部系统,有的人负责数据,有的人负责审核,有的人负责活动。所以这里需要一个授权模块,根据员工的角色或岗位,把哪些子系统能用、哪些不能用,一次性配置好。不用每个系统内部都再做一套权限判断。

6. IP 白名单控制

因为这个后台等于是公司所有内部系统的"总入口",安全性必须要做。除了账号密码之外,我还加了 IP 白名单,限制只有公司网络或特定来源的机器才能访问,避免被外部恶意撞库。

7. 操作日志

作为一个完整的后台,操作审计必须有。谁登录了、谁改了子系统配置、给谁授权了什么权限,这些都得记录下来,出了问题也能快速追踪。

整体拆下来,这几个模块基本能覆盖企业里一个统一门户该做的事:集中入口、统一账户、系统注册、权限分发、安全防护和审计。后面再结合单点登录的流程,就能构成一套完整的后台管理中心。


后台设计解读

前面我主要讲的是整体的架构和思路,这里我们就结合后台页面,一块一块讲我这个门户到底是怎么设计的。

1. 主页(系统入口总览)

我们登录进来之后看到的就是首页,也是除了管理员之外的员工主要使用的页面。基本功能就是把对应员工有权限的所有子系统按业务分类展示出来。我们公司内部子系统比较多,有研发相关的、有审核相关的、有监控相关的,我就按业务模块分了栏目,这样不至于全部堆一起太乱。如下图所示:

每个系统都是一张独立的卡片,卡片上会展示系统名称和一句简介,让同事一眼能看出是谁的后台、干什么的。如果同事要进入某个后台,直接点这张卡片就行了。

点击后,门户会去帮他生成一个 code,再带着 code 跳转到对应子系统,子系统再用这个 code 完成登录。

对用户来说根本不需要关心这些,体验就是:点一下就能进入后台,不需要再登录第二次。

这个设计能让常用后台的人非常省心,也能避免"换设备了所有后台入口都找不到"的情况。

2. 用户管理(内部员工统一账号体系)

公司内部的后台系统多了之后,最容易乱的就是账号体系:

每个系统自己做一套账号密码,权限也各管各的,部门越多越难维护。

所以在门户里,我专门做了一个统一的用户管理模块,把所有员工的账号全部集中到一处管理。

如下图所示,用户列表上能看到每个人的基础信息(用户名、邮箱)、角色、状态、最近登录时间、是否在线等等。新同事入职,只需要在这里建一个账号,他就能直接进入自己有权限的所有后台,不用再到每个系统里重复创建。

另外,因为这个门户本质上是所有内部后台的"总入口",安全性要求比普通业务系统高得多,所以我在用户体系里加了两个非常关键的功能:

① 双因子验证(2FA)

后台入口一旦泄露,相当于所有子系统的门都被打开了。

内部环境里又经常出现密码被共享、弱密码、密码泄露没人知道这些情况,所以给账号加一道 2FA 可以大幅提高安全性。

只要绑定了 2FA,就算密码被别人拿到了,也进不来。

② 踢下线(强制下线)

这个在内部环境特别重要,比如:

  • 发现账号异常登录
  • 某个用户正在操作不该操作的东西
  • 临时需要马上收回某人的权限
  • 用户离职但还保持登录状态

管理员在后台点一下"踢下线",当前用户所有 token 会立刻失效,需要重新登录才能继续。

对于内部后台来说,这功能简直是救命的。

总体来说,用户管理模块就是两个字:统一

把账号、权限、安全都统一起来管理,后面所有子系统都能直接复用这一套体系,再也不会出现"密码在哪""这个系统是谁管理的"这种混乱情况。

3. 系统管理(子系统注册)

这个模块其实就是整个平台的"系统字典"。

我们公司的后台系统本来就多,而且大部分都是私有化部署,像测试后台、正式后台、日志平台、监控平台、构建发布平台......越做越多,不统一管理的话,谁都记不住哪里还有个后台。

所以我在门户里做了一个非常关键的功能:系统注册

作用就是告诉门户------"我们公司目前到底有哪些后台系统,它们长什么样、入口在哪、怎么访问"。

如下图所示

每一个后台系统在这里都可以独立配置:

  • 系统名称 ------ 展示在首页卡片上的名字
  • 模块 ID ------ 后续子系统接入时会用到
  • 系统分类 ------ 比如后台系统、监控系统、日志系统、构建发布等,首页会按分类分组
  • 入口地址 ------ 点击卡片跳转过去的 URL
  • 状态 ------ 是否启用
  • 创建/更新时间 ------ 方便运维排查
  • 操作按钮(编辑 / 禁用 / 删除)

至于我们为什么要做成一个独立的系统管理模块?

因为内部后台数量一旦多起来,我们不可能靠写死配置或靠脑子记住。

所以有了这个列表,任何系统变更都可以统一管理,比如:

  • 某个后台换域名了 → 在这里改一次,全公司生效
  • 新做了一个子系统 → 在这里点"新增系统",首页就会自动展示
  • 某个测试环境要临时下线 → 在这里点"禁用"

门户首页上的所有"卡片",都是直接从这个列表动态渲染出来的,

而不是写死在代码里。这样后期系统再多,也不会乱。

总的来说,"系统管理"就是整个平台的"系统资产中心"。

后台多也不怕,只要这里登记清楚,整个门户就能随时扩展、随时更新。

4. 系统健康检查(探活监控)

既然门户已经把所有子系统都统一展示出来了,那每个系统现在是不是"在线",我们肯定也要能看到。

不然同事一点击某个后台,结果发现打不开,还以为是自己网络问题,其实系统早就挂了。

所以我在门户里做了一个 健康检查模块(如下图所示):

  • 每个子系统都会配置自己的健康检查地址(一般就是 /health/actuator/health
  • 门户会定时去请求这些地址
  • 根据响应情况来判断系统是否正常
  • 还会记录响应时间、最后检测时间,方便排查

比如图里这样:哪个系统在线、延迟多少、哪个访问失败,一眼就能看出来。

这个模块对多后台、多环境特别有用。因为很多时候某些后台并不是我们负责的,有了这个页面之后,就能快速判断问题到底是在入口、在系统本身、还是在网络。

健康检查探活接口大概是怎么做的?

做法其实很简单:

  1. 系统管理里登记健康检查地址 (比如 https://xxx.com/health

  2. 后台用定时任务(Scheduled)每隔 N 秒发一次 GET 请求

  3. 根据返回结果更新数据库状态

    • 200 → 正常
    • 超时 / 异常 → 异常
  4. 前端展示状态、响应时间、最后检测时间

  5. 用户可以点击"刷新"按钮立刻重新探活一次

整个逻辑不复杂,但实际效果非常有用。

尤其是在多后台、多环境的场景下,像系统偶发超时、某个环境挂掉、或者某个服务正在发布,都能在这里第一时间看出来,省得点进去才发现打不开。

如果想做得更完善一点,其实还可以加上系统通知 能力,比如某个后台连续探活失败就自动发企业微信/邮件提醒。

这次的 Demo 我暂时没写这一块,但如果作为正式内部平台,这个功能非常建议做上,会更接近大厂的运维体系。

5. 用户授权(控制员工能进入哪些后台)

有了统一的用户体系之后,下一步就是做权限分配,也就是控制"谁能访问哪些后台系统"。

这个模块是整个门户的核心之一,基本所有权限相关的逻辑都从这里开始。

界面大概是下面这样(如下图所示):

左侧是所有员工列表,点选某个员工后,右侧会展示他当前已经拥有的后台系统权限,同时也可以继续新增、取消某些系统的访问权限。

整个授权过程非常直观:

  • 选中左侧的员工
  • 右侧自动列出所有可授权的系统
  • 已授权的会高亮显示
  • 想给谁开权限、关权限,直接点一下即可
  • 最后点击"保存授权"

这种设计对我们的内部场景非常适合,因为每个人的职责差异都很大,比如:

  • 新来的运营可能只需要数据后台,不需要 Prometheus 或 GitLab
  • 技术团队需要多个系统权限,比如构建平台、监控中心、日志分析
  • 财务、审核、发布等部门各自有完全不一样的后台入口
  • 一些敏感系统(例如运维系统、生产管理后台)只能开放给特定人员

所有这些都可以在这个页面里统一管理,而不需要让每个子系统再做一套自己的权限体系。

这一点非常关键:子系统不需要再维护自己的用户表、角色表、权限关系表等复杂逻辑,所有权限都由门户来管理。

子系统只需要做一件事:收到 code → 向认证中心换用户信息 → 判断是否允许进入即可。

授权逻辑简单、统一、可控,也非常符合公司内部多后台场景的需求。

6. IP 白名单(安全控制)

因为这个门户等于所有内部系统的总入口,所以安全必须加强。我做了一个 IP 白名单模块,类似公司的访问控制:

  • 只有公司网络、内网服务器、特定地址才能访问
  • 不在白名单的直接拦截

这样可以避免外部恶意访问,比如撞库攻击、接口扫爆、弱密码尝试等等。

7. 操作日志(审计记录)

后台系统必须有操作日志。

不管是修改了权限、添加了用户、更新了子系统配置,还是登录、退出,都要记录下来:

  • 谁操作的
  • 操作内容是什么
  • 操作时间
  • 操作来源 IP

一旦出现问题,可以快速定位到具体的人和操作。


数据表设计

说完后台的几个管理模块之后,接下来就要看它们底层的数据结构了。

其实整个统一门户的平台数据结构并不复杂,真正的核心也就四张表,分别负责:用户、系统分类、系统信息、用户授权。

我这里把关键表结构都贴出来,并结合业务说明一下设计思路。

1. 用户信息表:sys_user

这是整个门户的账号根表,所有员工的登录信息都在这里统一管理。

为什么要这样设计呢?

因为门户是所有后台的入口,一个员工有且只应该在门户这里维护一次账号,不再让每个子系统重复建表、重复维护账号。

这里会存储:

  • 用户名(唯一)
  • 加密后的密码
  • 邮箱、手机号
  • 2FA 密钥(google_secret)
  • 状态(启用/禁用)
  • 最近登录时间
  • 创建时间、更新时间
sql 复制代码
CREATE TABLE `sys_user` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `username` varchar(64) NOT NULL COMMENT '用户名(唯一)',
  `password` varchar(255) NOT NULL COMMENT '加密后的密码',
  `email` varchar(128) DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(32) DEFAULT NULL COMMENT '手机号',
  `google_secret` varchar(64) DEFAULT NULL COMMENT '2FA 密钥',
  `avatar` varchar(255) DEFAULT NULL COMMENT '头像地址',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:1启用,0禁用',
  `last_login_at` datetime DEFAULT NULL COMMENT '最近登录时间',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';

2. 系统分类表:sys_category

首页展示需要分组,否则几百个系统放一起会乱成一锅粥。

所以我们用分类表用来定义系统的业务分组,比如:

  • 后台系统
  • 构建发布
  • 数据分析
  • 日志系统
  • 监控系统

主要用于显示,不涉及权限。

sql 复制代码
CREATE TABLE `sys_category` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `category_code` varchar(64) NOT NULL COMMENT '分类标识(英文缩写,如 backend、deploy)',
  `category_name` varchar(128) NOT NULL COMMENT '分类名称(中文,如 后台系统)',
  `sort_no` int(11) DEFAULT '0' COMMENT '排序号(越大越靠前)',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注说明',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `category_code` (`category_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统分类表';

3. 接入系统信息表:sys_application

这张表记录 公司所有的后台系统,是统一门户最关键的一张。

每个子系统都会在这里登记:

  • 系统 ID(英文)
  • 系统名称(中文)
  • 系统介绍
  • 入口地址(点击卡片跳过去用)
  • 图标地址
  • 状态(启用/禁用)
  • 分类 ID(关联 sys_category

门户首页那些卡片、图标、分组展示,全部来源于这张表。

sql 复制代码
CREATE TABLE `sys_application` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `category_id` bigint(20) unsigned DEFAULT NULL COMMENT '分类ID(关联sys_category)',
  `app_id` varchar(64) NOT NULL COMMENT '系统唯一标识(英文缩写)',
  `app_name` varchar(128) NOT NULL COMMENT '系统名称(中文名)',
  `app_desc` varchar(255) DEFAULT NULL COMMENT '系统副标题/描述',
  `entry_url` varchar(255) NOT NULL COMMENT '系统入口地址',
  `icon` varchar(255) DEFAULT NULL COMMENT '系统图标地址',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:1启用,0禁用',
  `sort_no` int(11) DEFAULT '0' COMMENT '分类内排序号',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `app_id` (`app_id`),
  KEY `fk_app_category` (`category_id`),
  CONSTRAINT `fk_app_category` FOREIGN KEY (`category_id`) REFERENCES `sys_category` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='接入系统信息表';

4. 用户授权关系表:sys_user_application

所有权限控制都依赖这张表。

这张表的作用是:记录某个员工可以访问哪些后台系统

关系是多对多:

  • 一个员工可以有多个系统权限
  • 一个系统也可以授权给多个员工

所以用了 (user_id, app_id) 唯一索引来避免重复授权。

子系统登录时,通过门户验证 code,就能知道该用户是否有权限进入。

sql 复制代码
CREATE TABLE `sys_user_application` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
  `app_id` bigint(20) unsigned NOT NULL COMMENT '系统ID',
  `granted_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '授权时间',
  `granted_by` varchar(64) DEFAULT NULL COMMENT '授权人',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_app` (`user_id`,`app_id`),
  KEY `fk_app` (`app_id`),
  CONSTRAINT `fk_app` FOREIGN KEY (`app_id`) REFERENCES `sys_application` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_user` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户与系统授权关系表';

后端接口设计

在实现门户免登子系统之前,我们后端接口设计其实只需要准备两个核心接口:

  1. 创建授权码接口 (认证中心 → 子系统)
    用来生成一次性 code,并发给子系统。
  2. 校验授权码接口 (子系统 → 认证中心)
    子系统拿 code 来换取用户信息,完成免登录。

整个流程就是围绕 "发 code → 用 code 换用户信息" 两件事来展开的,这也是我们这套轻量化 SSO 的核心机制。

只要把这两个接口打通,所有语言的子系统(Java / Go / PHP / Python)都能用统一方式接入,换句话说,我们这套机制并不是把所有子系统都彻底绑死在认证中心上,也不要求它们完全放弃自己的登录体系。每个子系统依然可以保留各自原有的账号密码登录逻辑,只是额外多封装了一步"通过门户授权码免登"的逻辑。

这样做有两个好处:

  • 门户可以实现统一入口、一键免登
  • 子系统也能独立运行、独立登录,不依赖门户也能使用

我们最终要的不是把所有系统的登录逻辑全部迁到认证中心,而是实现一种 松耦合的 SSO

------能统一就统一,不能统一也不影响各自运行。

1. 创建授权码接口(/sso/code/create)

要实现门户免登子系统,核心就是生成一个一次性的授权码(code),然后子系统再拿着这个 code 去认证中心换取用户信息。这个接口就是整个 SSO 流程的第一步。

接口地址:

json 复制代码
POST /sso/code/create

流程简单说就是:

用户点击门户某个系统卡片 → 门户向认证中心要一个 code → 前端把 code 拼到系统 URL → 子系统拿 code 来换用户信息 → 完成免登。

前端调用逻辑

当用户在门户首页点击某个系统卡片时,会触发前端封装的 goSystem 方法:

javascript 复制代码
/** 点击进入系统 */
const goSystem = async (item) => {
  if (!item.link) {
    ElMessage.warning("该系统暂未配置访问地址");
    return;
  }

  if (!user.id) {
    ElMessage.error("用户未登录或信息缺失");
    return;
  }

  try {
    const res = await createSSOCode(user.id, item.code);  // 调用 /sso/code/create
    
    if (res.success && res.data?.code) {
      const url = `${item.link}?code=${res.data.code}`;
      window.open(url, "_blank");  // 带 code 跳转到子系统
    } else {
      ElMessage.error(res.message || "生成登录 code 失败");
    }
  } catch (err) {
    ElMessage.error("认证中心连接失败");
  }
};

我们可以看到,前端其实就做了三件事情:

  1. 验证用户是否已登录门户
  2. 调用 /sso/code/create 生成一个短期有效的授权码
  3. 把 code 拼接到子系统 URL,例如:
ini 复制代码
https://admin.xxx.com?code=asf8sdf87dsf0sd8f0

子系统收到这个 code 后,就可以开始免登录流程。

后端调用逻辑

后端的 createCode 方法主要分为 5 步:

java 复制代码
@Override
public Map<String, Object> createCode(HttpServletRequest request, Map<String, Object> body) {
    // 1. 验证 Token 登录态
    Long userId = (Long) request.getAttribute("userId");
    if (userId == null) {
        throw new RuntimeException("未登录或 Token 无效");
    }

    String redisKey = SsoRedisKeys.userLogin(userId);
    User cachedUser = redisUtils.get(redisKey, User.class);
    if (cachedUser == null) {
        throw new RuntimeException("登录状态已过期,请重新登录");
    }

    // 2. 校验系统信息
    String appId = body.get("appId").toString();
    SysApplication app = applicationMapper.selectByAppId(appId);
    if (app == null) throw new RuntimeException("应用不存在:" + appId);
    if (app.getStatus() == 0) throw new RuntimeException("应用已禁用:" + app.getAppName());

    // 3. 校验用户权限
    if (!userHasPermission(userId, appId)) {
        throw new RuntimeException("用户无权访问系统:" + appId);
    }

    // 4. 生成一次性 code
    String code = UUID.randomUUID().toString().replace("-", "");

    String authRedisKey = SsoRedisKeys.authCode(code);
    Map<String, Object> authInfo = new HashMap<>();
    authInfo.put("userId", userId);
    authInfo.put("appId", appId);

    // 将 code → 用户信息 写入 Redis,1 分钟自动失效
    redisUtils.set(authRedisKey, authInfo, 60);

    // 5. 返回给前端
    Map<String, Object> result = new HashMap<>();
    result.put("code", code);
    result.put("expire", 60);
    result.put("appId", appId);
    return result;
}

其中这个 code 的作用其实很单纯,就是给门户和子系统之间当一个临时的跳转凭证,所以一定要做到短效、一次性、用完即删。它不应该长时间存在,更不应该落库,否则就失去了临时授权的意义。通常用户点击卡片后几百毫秒就能完成跳转,60 秒的有效期已经足够,同时还能避免被拦截后延迟使用、被重放攻击或者被人恶意构造 URL 利用。越短的有效期,就越能减少被利用的风险,所以 60 秒其实是一个安全性和可用性都比较平衡的选择。

最后当认证中心生成了 code 之后,其实并不是简单地返回一个字符串,而是同时在 Redis 里保存了一条和这个 code 绑定的临时授权信息。像图里这样:

Redis 里存的是 appId 和 userId,也就是"哪个用户要进入哪个系统"。这样做的原因很简单:后续子系统带着 code 来换用户信息时,认证中心只需要通过 code 找到这一条 Redis 记录,就能知道这次授权对应的用户是谁、目标系统是哪一个。整个过程不依赖数据库,也不需要在 URL 中暴露敏感信息,而是完全走 Redis 的短期缓存,既轻量又安全。换句话说,code 只是一个随机字符串,本身没有任何意义,真实的授权数据全部在 Redis 里,一旦子系统来验证,认证中心就能立即把 userId 拿出来做下一步处理。

2. 回调验证接口(/sso/code/verify)

当用户从门户跳到子系统时,URL 里会带一个 code,子系统拿到这个 code 后需要到认证中心来"换用户信息",这个过程就是通过 /sso/code/verify 完成的。

这个接口的目的很简单:
确认这个 code 是否真实、有效、没过期、没被用过,并最终告诉子系统:当前用户是谁。

接口地址:

bash 复制代码
POST /sso/code/verify

伪代码如下:

java 复制代码
public Map<String, Object> verifyCode(HttpServletRequest request, Map<String, Object> body) {

    // 解析参数
    String code = String.valueOf(body.get("code"));
    String appId = String.valueOf(body.get("appId"));

    if (code == null || code.isBlank() || appId == null || appId.isBlank()) {
        throw new RuntimeException("参数缺失:code 或 appId 不能为空");
    }

    // 校验 Redis 是否存在授权码
    String redisKey = SsoRedisKeys.authCode(code);
    Map<String, Object> authInfo = redisUtils.getObject(redisKey, Map.class);
    if (authInfo == null) {
        throw new RuntimeException("授权码无效或已过期");
    }

    // 校验 appId 一致性
    String storedAppId = (String) authInfo.get("appId");
    if (!appId.equals(storedAppId)) {
        throw new RuntimeException("授权码与应用不匹配");
    }

    // 查询用户信息
    Long userId = Long.parseLong(authInfo.get("userId").toString());
    User user = userMapper.selectById(userId);
    if (user == null) {
        throw new RuntimeException("用户不存在");
    }

    // 校验登录态是否有效
    String loginKey = SsoRedisKeys.userLogin(userId);
    User cachedUser = redisUtils.get(loginKey, User.class);
    if (cachedUser == null) {
        throw new RuntimeException("登录状态已过期,请重新登录");
    }

    // 一次性使用后删除
    redisUtils.delete(redisKey);

    // 返回身份信息
    Map<String, Object> result = new HashMap<>();
    result.put("userId", user.getId());
    result.put("username", user.getUsername());
    result.put("appId", appId);
    return result;
}

当子系统把 code 发过来后,认证中心会先检查这个 code 是否存在。因为 code 是一次性的、短期有效的,所以就在 Redis 里搜索是否有对应的授权记录。如果查不到,说明 code 要么过期了,要么已经被用过了,这种情况就直接拒绝登录。

如果 Redis 里确实有值,里面包含 userId 和 appId 两项关键信息。这时认证中心会再校验一下传过来的 appId 是否和 code 所属的 appId 一致,防止有人把 code 拿去访问别的系统。

接下来就是根据 userId 查用户信息,并且再校验一次用户的登录态是否有效,避免用户已经在门户退出,却还能继续使用旧的 code。

所有校验都通过后,这个 code 就算使用完了,会立即从 Redis 里删除,确保只能用一次。最后返回最小必要的身份信息,包括 userId、用户名和 appId。子系统拿到这些信息后,就可以根据自身的逻辑生成本地 Token,完成免登流程。


子系统如何接入门户免登

前面两个接口把门户和认证中心之间的流程已经串起来了,接下来我们就要简单讲下子系统这边怎么接入。其实子系统的逻辑非常简单,就是判断 URL 上有没有 code,如果有,就把这个 code 拿到认证中心换成用户信息,然后由子系统自己生成一个本地 token。这样既能支持从门户免登,也不会影响子系统保持自己的独立登录体系,两种方式互不冲突。这个设计的好处是,子系统不用共享 Session、不需要共享 JWT Secret,也不依赖门户,各自维护好自己的登录体系就行了。

1)前端处理逻辑:在路由守卫里自动检测 URL 中的 code

子系统前端只需要在路由守卫里加一个简单的判断:如果当前没有 token,但 URL 上带着 code=xxx,那就说明用户是从门户点进来的,这时候就自动走 SSO 登录,把 code 发给子系统后端验证即可。

javascript 复制代码
router.beforeEach(async (to, from, next) => {
  const token = localStorage.getItem('token')
  const code = getQueryParam('code')

  // 自动 SSO 登录
  if (!token && code) {
    try {
      const res = await verifyCode(code)

      if (res.success && res.data) {
        localStorage.setItem('token', res.data.token)
        localStorage.setItem('user', JSON.stringify(res.data.user))

        ElMessage.success('SSO 登录成功')

        // 清除 URL 中的 ?code=xx 避免重复触发
        const cleanUrl = window.location.origin + to.path
        window.history.replaceState({}, '', cleanUrl)
        window.location.reload()

        next('/gray/config')
        return
      } else {
        ElMessage.error(res.message || 'SSO 登录失败')
      }
    } catch (err) {
      ElMessage.error('认证中心连接失败')
    }
  }

  // 已登录再访问登录页 → 直接跳首页
  if (to.path === '/login' && token) {
    next('/gray/config')
    return
  }

  // 未登录访问需要权限的页面 → 跳转登录页
  if (!to.meta.public && !token) {
    next('/login')
    return
  }

  next()
})

这段逻辑很直观:门户跳进来自动登录,不影响我们系统原有的用户名密码登录。

2)后端处理逻辑:收到 code → 去认证中心验证 → 生成本地 token

子系统后端也不复杂,收到前端传来的 code 之后,带着自己的 appId 去认证中心验证,认证中心确认 code 正常后,会返回用户最基本的身份信息,比如 userId、username。我们再根据这些信息生成自己的 JWT token,前端拿到 token 之后就算登录成功了。

伪代码整理如下:

java 复制代码
public Result<?> verifyCode(@RequestBody Map<String, Object> body) {
    String code = (String) body.get("code");

    // 当前子系统的 appId
    String appId = grayAppProperties.getAppId();

    // 认证中心地址
    String ssoUrl = "http://localhost:8085/sso/code/verify";

    // 构建请求参数
    Map<String, Object> payload = new HashMap<>();
    payload.put("code", code);
    payload.put("appId", appId);

    // 调用认证中心接口
    Map<String, Object> ssoResp = RestClient.postJson(ssoUrl, payload);

    if (ssoResp == null || !Boolean.TRUE.equals(ssoResp.get("success"))) {
        return Result.fail("认证中心验证失败");
    }

    // 认证中心返回的用户信息
    Map<String, Object> data = (Map<String, Object>) ssoResp.get("data");
    Long userId = ((Number) data.get("userId")).longValue();
    String username = (String) data.get("username");

    // 组合 JWT claims
    Map<String, Object> claims = new HashMap<>();
    claims.put("userId", userId);
    claims.put("username", username);
    claims.put("fromSSO", true);

    // 子系统本地生成 token
    String token = jwtUtil.generateToken(claims);

    // 返回给前端
    Map<String, Object> resultData = new HashMap<>();
    resultData.put("token", token);
    resultData.put("user", data);

    return Result.ok(resultData);
}

整个过程就是标准的 "code 换 token" 流程,不过我们并没有把 token 放到认证中心里,而是由各个子系统自己生成自己的 token,这样每个系统都保持独立、互不影响。前端一存,就算从门户免登成功了。

3)为什么这样设计?

我们在设计这部分的时候,核心点就是让每个子系统保持自己的独立性。我们只用 code 做一次性的身份凭证,然后各个子系统还是走自己的登录体系、自己的 token 校验,不需要共享密钥,也不需要耦合认证中心的结构。子系统既能支持门户单点进入,也能支持本地登录退出,而且逻辑非常清晰简单,任何语言都能接入,属于接入成本最低的一种模式。


单点退出(Logout)设计与实现

统一门户既然承担了统一登录的职责,那退出逻辑也必须做到统一。如果用户从门户点了退出,我们不希望出现一种情况:门户退出了,但他之前用 SSO 打开的子系统仍然保持登录状态,这样既不安全,也会造成体验不一致。所以我们需要补齐"单点退出"的能力,让用户在认证中心退出后,所有通过 SSO 进入的系统都自动失效。

单点退出的核心目标只有两个:一是让认证中心退出后能通知所有子系统;二是让子系统能够识别来自 SSO 的登录与本地登录是不同的,以免影响子系统的独立登录场景。我们采用的方案比较简单,不需要做传统 CAS 那样的"登出广播",而是直接利用 Redis 作一次性标记。认证中心退出时写一条 logout 标记,子系统在自己的 JWT 拦截器里检查这个标记,如果标记存在,就说明门户已经退出了,这时子系统应该让用户回到登录页。

1)认证中心退出:写入退出标记

认证中心退出逻辑很简单:删除登录状态,同时写一条带有 TTL 的 logout 记录,子系统看到这条记录就会认为用户已在门户退出。

伪代码如下:

java 复制代码
@Override
public Map<String, Object> logout(HttpServletRequest request) {
    Long userId = (Long) request.getAttribute("userId");
    if (userId == null) {
        throw new RuntimeException("未登录或 Token 无效");
    }

    // 删除登录状态
    String loginKey = SsoRedisKeys.userLogin(userId);
    redisUtils.delete(loginKey);

    // 写入退出标记
    long logoutTime = System.currentTimeMillis() / 1000;
    String key = SsoRedisKeys.userLogout(userId);
    redisUtils.set(key, logoutTime, 7 * 24 * 3600); // 7 天 TTL

    Map<String, Object> data = new HashMap<>();
    data.put("userId", userId);
    data.put("logoutTime", logoutTime);
    data.put("ttl", "7d");
    return data;
}

退出记录在 Redis 中大概是这样的:

bash 复制代码
sso:user:logout:1 → 1763620214

这条记录说明用户 1 在某个时间点触发过统一退出。子系统只要检查这条记录是否存在,就可以判断 SSO 登录是否还有效。

2)子系统拦截器:判断是否属于 SSO 登录,并检查退出状态

子系统自身有登录体系,所以不能看到 logout 标记就直接踢用户,否则会导致本地登录的用户也被强制退出。这里我们用的解决办法是,在生成 token 的时候我们加上一个 fromSSO 的标记,用来区分这次登录到底是门户过来的,还是本地登录的。

拦截器伪代码逻辑如下:

java 复制代码
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler)
        throws Exception {

    String token = req.getHeader("Authorization");
    if (token == null || token.isEmpty()) {
        ResponseUtil.writeJson(resp, Result.fail(401, "缺少 Token"));
        return false;
    }

    try {
        Claims claims = jwtUtil.parseToken(token);
        Long userId = Long.valueOf(String.valueOf(claims.get("userId")));

        Boolean fromSSO = claims.get("fromSSO", Boolean.class);

        // 仅拦截通过 SSO 登录的用户
        if (Boolean.TRUE.equals(fromSSO)) {
            String logoutKey = GrayRedisKeys.userLogout(userId);
            if (redisUtils.hasKey(logoutKey)) {
                ResponseUtil.writeJson(resp, Result.fail(401, "登录已过期,请重新登录"));
                return false;
            }
        }

        req.setAttribute("userId", userId);
    } catch (Exception e) {
        ResponseUtil.writeJson(resp, Result.fail(401, "Token 无效或已过期"));
        return false;
    }

    return true;
}

这样子系统的拦截器就非常清晰:如果是通过 SSO 进入的用户,就判断认证中心是否退出;如果是本地登录的用户,就不受认证中心的影响。我们用 fromSSO 字段把行为分隔得很清楚,既保护了 SSO 的一致性,也不会破坏子系统单独登录的使用场景。

3)前端响应拦截:自动处理 401,清理本地 token

子系统前端只需要在响应拦截器里判断 code 是否为 401,如果是,就清除本地存储并回到登录页面:

javascript 复制代码
service.interceptors.response.use(
  (res) => {
    const data = res.data;
    if (data && data.code === 401) {
      ElMessage.warning(data.message || "登录已过期,请重新登录");
      localStorage.clear();
      router.push("/login");
      return Promise.reject(new Error("Token 过期"));
    }
    return data;
  },
  (err) => {
    if (err.response && err.response.status === 401) {
      localStorage.clear();
      ElMessage.warning("登录已过期,请重新登录");
      router.push("/login");
    }
    return Promise.reject(err);
  }
);

有了服务端的退出标记和前端的自动清理,整体的单点退出流程就闭环了。

4)为什么必须加 fromSSO?

fromSSO 是整个单点退出中最关键的一个小细节,因为我们的整体架构是不强制子系统必须按照传统 SSO 方式,只能通过统一门户登录。每个系统都可以单独维护自己的本地登录逻辑,而门户 SSO 只是额外的能力。如果不区分 fromSSO,那么一旦门户退出,所有子系统的本地用户也会被强制退出,这明显不符合预期。

所以我们在生成 token 时加入了这样一个标记:

  • 子系统本地登录 → fromSSO = false
  • 从门户 code 登录 → fromSSO = true

也正是因为这个标记,子系统拦截器才能精准判断哪些登录需要被 SSO 退出影响,哪些不需要。

最后我这边贴一个简单的测试图我们来看下效果:

上面的这段动图可以很直观地看到整个单点退出机制的实际效果。我们从统一门户进入灰度中心时,灰度中心并没有做任何本地登录操作,而是完全依靠门户生成的授权码完成免登。因此,当我们在统一门户点击退出后,再回到灰度中心刷新页面,它会立即识别到"门户已经退出",从而自动清理自身登录态并回到登录页。

另外也可以看到,如果我们不是通过门户跳转,而是在灰度系统内部执行一次本地登录,那么它生成的 token 是本系统自己维护的,不携带 fromSSO 标记。这种情况下,即使门户已经退出,灰度系统依然保持独立登录状态,不会受到 SSO 退出的影响。

这样的设计目的就是减少耦合,不强制所有系统必须依赖统一门户统一登录。每个子系统依然可以根据业务场景选择单独登录或者 SSO 免登录,两种模式互不干扰。只有通过门户进入的用户才会受统一退出控制,本地登录的用户始终拥有独立的登录生命周期。 这样既能满足统一管理的需求,也保留了系统的独立性,整体结构既清晰又不互相牵制,适合多后台、多系统同时存在的企业环境。


其他扩展

我们做到这里,一个完整的 SSO(统一门户免登 + 单点退出)其实已经能稳定跑起来了,但如果系统一多、语言一杂、团队一大,就会出现一个问题:

每个子系统都要自己写一遍 SSO 登录、code 校验、token 生成、拦截器逻辑?

这样太浪费,维护成本也会飙升,所以接下来我们就需要把 SSO 的能力做成"可复用的模块",让所有系统都能无感接入。

1. Java 体系:直接封成 SDK,所有项目一行依赖即可用

对于 Java 系的项目来说(Spring Boot 为主),完全可以把所有 SSO 相关逻辑整理成一个 SDK,例如:

js 复制代码
sso-client-sdk
 ├── JwtUtil
 ├── SSOProperties(比如 appId、centerUrl)
 ├── SSOLoginFilter(自动处理 code 验证)
 ├── TokenInterceptor(自动处理 fromSSO 的退出检查)
 └── SSOClient(封装调用认证中心的 verify 接口)

发布到私服或 GitHub Packages 后,子系统只需要:

java 复制代码
<dependency>
    <groupId>com.xxx.sso</groupId>
    <artifactId>sso-client-sdk</artifactId>
    <version>1.0.0</version>
</dependency>

然后在配置文件中写:

yml 复制代码
sso:
  app-id: gray-center
  server-url: https://sso.xxx.com

整个 SSO 登录流程就自动接入了,不需要重复写逻辑,也不会在几十个项目里散落多个版本,后期升级、修复都能统一管理。

2. PHP / Go / Node.js 也同样可以做成 SDK

非 Java 的系统一般也会有同样需求,所以也可以做对应语言的 SSO 客户端库,让接入方式尽可能统一:

  • PHP:封装一个 SsoClient.php,代码验证、JWT 生成、token 拦截都在里面
  • Go:做成一个 module,比如 github.com/xxx/sso-client
  • Node.js / Express:封装为中间件 sso-client-middleware

只要系统接入 SDK,就能:

  • 自动解析 URL code
  • 自动调用认证中心 /sso/code/verify
  • 自动创建本地 token
  • 自动接入退出机制
  • 自动拦截无效 token

整个流程不需要人工重复实现,提高一致性,也减少出错概率。

3. 非私有化系统接入:像 Jenkins、GitLab,这类系统不方便改代码怎么办?

很多工具系统(Jenkins、GitLab、Prometheus、Grafana)是第三方部署的,不是我们写的代码,所以它们不可能接我们的 SSO verifyCode 逻辑。

但对于这类系统,其实登录控制也不复杂,我们可以用一种"轻量级通行证模式"处理。

作为例子:

门户跳转到 Jenkins 时,不用 code,不用 verify,只要模拟用户已登录状态即可。

比如:

  • 发一个带登录 Cookie 的 URL
  • 带一个 access_token 过去
  • 带 basic_auth 信息
  • 或者直接反向代理注入 header(企业常用)

例如对于 Jenkins:

ini 复制代码
https://jenkins.xxx.com/login?redirect=/?token=xxxx

或者 Nginx 代理:

sql 复制代码
proxy_set_header X-Auth-User $user;

私有化系统走严格的 SSO 认证

非私有化系统走轻量登录模拟

这样就不会为了兼容某些系统搞得结构很乱。

4. 我们这样设计的核心目的是什么呢?

总的来说:

保留统一门户的能力,同时不给子系统增加负担,也不强制所有系统必须依赖 SSO。

也就是说:

  • Java、PHP、Go 等项目:用 SDK 自动接入,低成本
  • 第三方工具:用轻量方案模拟登录,不去硬塞 SSO
  • 子系统既能通过门户免登,也能单独登录
  • 单点退出只影响 SSO 入口,不影响本地登录
  • 整体结构不耦合、可自由组合、适配能力强

这套模式对于我们真实的企业内部环境其实非常友好,尤其适合"多后台、多语言、多环境"的公司,不会因为引入 SSO 而把所有系统都搞得绑死在一起。

其他:分布式场景下的部署注意事项

我们做的这个系统在单机环境里跑 SSO 没啥问题,但只要上了真实公司环境,基本都是多实例、多服务的分布式部署,这时候有两个点必须提前规划好。

第一是登录态要统一存储 。不能让某台机器自己管自己的 Session,否则用户访问 A 机器登录成功,切到 B 机器就变成未登录,体验会直接崩掉。第二是授权码 code、用户登录态、单点退出记录这些信息都必须放到一个所有实例都能访问的地方。因此我们整个 SSO 体系都统一使用 Redis,不管认证中心有多少台机器、子系统有多少个实例,它们取出来的 userLogin、code、logout 信息都是一致的。

另外,如果子系统、门户有多环境(dev/qa/prod),一定要在 Redis key 前缀上隔离清楚,避免多环境混用导致奇怪的问题,比如 A 环境退出把 B 环境也踢掉了。

最后,如果公司规模大一些,Redis 也应该部署成集群或哨兵模式,避免它成为整个 SSO 的单点故障。总结来说就是一句话:SSO 本质上是无状态的,所有共享状态都统一收敛到 Redis,系统才能横向扩容、保证一致性。

为什么我没使用 Session?

其中有一点我觉得Session 最大的问题就是跟机器绑定。只要服务有多台实例,Session 就绝对会出现"不一致"的情况,除非我们上粘性会话(sticky session)或者把 Session 做成集中式存储,但那其实又变成自己造了一个简化版 Redis,维护成本还不如直接用 Redis。本质原因就是 Session 天然适合单机,不适合分布式的后台环境。

那为什么不把 code 存数据库呢?

code 是一次性的、秒级生命周期的东西,放数据库完全没有意义。数据库适合存长期、有价值的记录,而不是这种用完即删的临时凭证。而且数据库写入本身就慢,频繁插入、删除不仅占 IO,也会撑大 binlog,还会带来事务锁竞争。SSO 整个流程讲究"快",一旦数据库参与进来,延迟会立刻被放大。

更关键的是,code 要做到一次性删除,这种频繁、密集、瞬时的小操作特别适合内存型存储,而不是关系型数据库。

所以从一开始我们就把 Redis 当作整个 SSO 的"状态中心",门户、认证中心、子系统都可以横向扩容,而状态又是统一的,这样架构才能稳定跑起来。


最后

做到这里,其实这套统一门户 + SSO 的体系就算完整跑通了。这样设计之后,平时最烦的那几个问题基本都没了:不用再在浏览器贴一排标签页,也不用记一堆账号密码;老板再也不会隔三差五地来问"那个后台地址给我发一下";换个电脑也不用翻半天聊天记录去找某个系统的入口。所有后台都汇总在一个门户里,它就成了公司内部的"总入口"。

大家只要记一个地址、登录一次,就能进去自己有权限的所有系统。权限集中管理,系统集中展示,健康检查也集中显示,整套体验从零散变成了清清爽爽的一套。更重要的是,这套方案没有强制所有子系统完全依赖 SSO,谁愿意走免登录就走免登录,谁需要单独登录也完全不影响,互不耦合、互不干扰,用起来非常灵活。

从开发的角度看,把 SSO 封成 SDK 后,新项目接入变成一件非常轻松的事情,几分钟就能完成,不用每个项目重复写一遍 code 校验、token 生成、拦截器这些逻辑。后面系统越来越多、环境越来越多、团队越来越大,也不会出现登录体系混乱的情况。这种"统一但不死绑"的方式,特别适合中小团队在多后台、多语言、多环境的实际场景里长期使用。

按照这个架构继续扩展,其实还能做很多事,比如后台导航、内部搜索、快捷入口、消息通知、系统巡检等等,都可以往上叠。总之,这套统一门户可以慢慢成长成公司内部稳定好用的一站式入口,真正解决我们日常被多个后台折腾的那些麻烦事。终于不用谁都来找我要地址了...

相关推荐
~无忧花开~3 小时前
JavaScript实现PDF本地预览技巧
开发语言·前端·javascript
qq_12498707533 小时前
基于springboot的建筑业数据管理系统的设计与实现(源码+论文+部署+安装)
java·spring boot·后端·毕业设计
也许是_3 小时前
架构的取舍之道:在微服务的“混乱”中建立秩序
微服务·云原生·架构
小时前端3 小时前
“能说说事件循环吗?”—— 我从候选人回答中看到的浏览器与Node.js核心差异
前端·面试·浏览器
IT_陈寒3 小时前
Vite 5.0实战:10个你可能不知道的性能优化技巧与插件生态深度解析
前端·人工智能·后端
SAP庖丁解码3 小时前
【SAP Web Dispatcher负载均衡】
运维·前端·负载均衡
z***3353 小时前
SQL Server2022版+SSMS安装教程(保姆级)
后端·python·flask
天蓝色的鱼鱼4 小时前
Ant Design 6.0 正式发布:前端开发者的福音与革新
前端·react.js·ant design
HIT_Weston4 小时前
38、【Ubuntu】【远程开发】拉出内网 Web 服务:构建静态网页(一)
linux·前端·ubuntu