找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

606

积分

0

好友

84

主题
发表于 昨天 07:10 | 查看: 3| 回复: 0

业务背景

在项目多以微服务架构拆分的背景下,一个业务往往会衍生出多个后台管理系统。尤其是在需要私有化部署的场景中,通常还需要区分测试环境、正式环境,并辅以独立的日志监控、运维工具与发布系统。随着时间的推移,各种子系统越建越多,部署分散在不同服务器上,管理变得异常繁琐。

当需要维护多个项目时,要记住所有后台系统的地址成为了一大难题。常用的做法是将入口固定在浏览器标签栏,但一旦更换设备、重装浏览器或清理缓存,这些入口便全部消失,只能重新翻找聊天记录或文档,效率极低。

对于管理者而言同样不便。他们往往不会主动收藏后台地址,每当需要查看数据时,都需要反复向开发人员索要具体的访问链接和账号密码,沟通成本很高。

为了解决这些问题,一个将零散后台统一管理的“统一门户管理中心”应运而生。它将所有子系统的入口整合在一个页面中,用户只需登录一次,即可根据权限访问所有关联的后台,无需记忆多个链接或重复登录。这对于管理者、运营人员以及开发团队都极大地提升了效率,也为后续系统的扩展管理提供了便利。

设计思路

在设计之初,单点登录(SSO)机制是首要考虑的方向。传统的SSO流程是:用户访问A系统,未登录则被重定向至认证中心,登录成功后再携带凭证返回A系统;当用户再访问B系统时,由于已在认证中心登录,便可实现免登。

这一机制本身成熟可靠,但与我们的内部场景存在差异。传统SSO是“先访问系统,再触发登录”,而我们的内部后台使用习惯是“先登录统一门户,再从门户导航至目标系统”。用户不会直接访问某个具体的子系统入口。

因此,完全照搬面向外部业务的传统SSO流程并不完全契合。传统SSO要求每个子系统都支持协议对接,涉及重定向、票据验证和登录态同步,改造成本高,且对于公司内部技术栈不统一(Java、PHP、Go及第三方系统并存)的情况而言,实施难度和风险都很大。

最终的思路是采用一种更轻量、更贴合内部使用习惯的方案。既然用户路径是从门户开始,那就以门户为中心集中处理登录。当用户从门户进入子系统时,由门户生成一个一次性授权码(code),子系统凭此code向独立的认证中心换取用户信息,然后在本地生成自身的会话令牌(Token)。

这种设计具备以下优势:

  • 用户体验上仍是单点登录,一次登录即可通行。
  • 子系统改造量小,无需支持完整的SSO协议。
  • 职责清晰:门户负责入口导航,认证中心负责身份验证。
  • 对多技术栈兼容性好,接入成本远低于传统SSO。

简而言之,这是一种结合了“统一入口”与“简化授权”的轻量化方案,特别适合内部私有化部署、多后台系统的场景。

方案选择

在确定技术方向前,评估了几种常见的授权模式:

  1. 统一Token(如JWT):门户登录后生成一个Token,所有子系统共用。问题在于需要共享密钥,安全风险高,且各系统对Token的校验逻辑和字段要求可能不同,耦合性强。
  2. OAuth2授权码模式:流程标准但重量级,要求每个子系统实现完整的OAuth2客户端逻辑,对于内部系统而言过度设计,实现复杂。
  3. 传统SSO/CAS:如前所述,其“从子系统出发”的流程与我们的“从门户进入”路径不符,子系统改造成本大。

综合对比后,选择了 “统一门户 + 一次性授权码(Code)” 的组合方案:
用户在门户统一登录;点击某个子系统入口时,门户生成一个一次性Code并携带跳转;子系统用该Code向认证中心换取用户信息,然后在本地生成自己的Token。

此方案在安全性、扩展性和接入成本之间取得了良好平衡,不绑定特定技术栈,非常适合内部异构系统环境。

整体流程如下:

统一门户SSO流程图

用户访问统一门户并登录。登录成功后,门户展示其有权限的所有后台系统。用户点击某个系统入口时,门户向认证中心申请一个一次性Code,并携带此Code将用户重定向至目标子系统。
子系统获得Code后,请求认证中心验证Code并换取用户信息。随后,子系统在本地生成一个Token,用于后续该子系统内的所有请求鉴权,不再依赖门户或认证中心。

此模式用户体验等同于单点登录,实现上却比传统SSO轻量,无需各子系统对接复杂的重定向流程,更适合系统多、语言杂、环境分散的内部场景。

后台设计思路

作为承载所有内部后台的入口,统一门户本身应是一个功能完备的后台系统。我们围绕内部使用场景,将其拆分为以下几个核心模块:

「1. 主页(系统入口总览)」
门户的核心页面,按业务分类展示用户有权限访问的所有子系统。用户点击卡片即可一键跳转,无需记忆或收藏分散的链接。

「2. 用户管理」
建立统一的员工账号体系。新员工入职只需在此注册一次,所有子系统均可复用该身份信息,避免各系统维护独立的账号体系。

「3. 系统管理(子系统注册)」
作为整个平台的“系统字典”,用于登记所有内部后台系统(如日志平台、监控系统、发布系统等)。任何新系统上线或旧系统变更,都在此统一配置,门户首页据此动态渲染。

「4. 系统健康检查」
门户集中展示入口,自然需知晓各子系统状态。此模块定时探测子系统的健康接口(如/health),实时展示系统在线状态、响应时间,便于运维及时发现问题。

「5. 用户授权」
集中配置员工与后台系统之间的访问权限。根据员工的角色或职责,一次性配置其可访问的系统列表,无需各子系统内部再实现一套权限逻辑。

「6. IP白名单控制」
作为总入口,安全加固必不可少。通过IP白名单限制访问来源,仅允许公司内网或特定IP访问,有效防止外部恶意访问。

「7. 操作日志」
完备的审计功能,记录所有关键操作(登录、权限变更、系统配置修改等),确保操作可追溯。

这些模块共同构成了一个覆盖集中入口、统一账户、权限分发、安全防护和操作审计的企业级统一门户。

后台设计解读

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

员工登录后看到的首页,按业务模块分类展示所有有权限的子系统卡片,每张卡片包含系统名称和简介。
门户主页示意
用户点击卡片后,门户后端会为其生成授权Code并自动跳转至子系统。对用户而言,体验就是「点击即进入,无需二次登录」,彻底解决了入口分散难寻的问题。

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

解决多系统账号体系混乱的问题。所有员工账号在此集中管理,支持查看基础信息、角色、状态等。
用户管理列表
鉴于门户是总入口,安全性要求极高,因此增强了两个关键功能:
「① 双因子验证(2FA)」:即使密码泄露,未经2FA验证也无法登录,极大提升安全性。
「② 强制下线」:发现账号异常或需紧急收回权限时,管理员可立即使用户所有会话失效。
此模块的核心是「统一」,为所有子系统提供一套可靠的底层身份体系。

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

用于登记和管理公司所有后台系统资产。
系统管理列表
每个系统独立配置,包括名称、唯一标识(模块ID)、分类、入口地址、状态等。门户首页的展示内容完全由此模块动态驱动。任何系统变更(如域名更改、新系统上线、临时下线)都在此统一操作,实现了系统资产的集中化、配置化管理。

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

统一展示各子系统的可用性状态。
健康检查面板
每个子系统需配置健康检查接口(如/health)。门户通过定时任务探测这些接口,并根据响应状态(正常、超时、异常)更新系统状态,同时记录响应时间和最后检测时间。该功能对于多后台、多环境下的运维巡检非常实用,能快速定位系统级问题。

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

权限分配的核心模块,控制每位员工可访问的系统范围。
用户授权界面
操作直观:选择左侧员工,右侧列出所有可授权的系统,已授权的系统高亮显示。通过勾选即可完成权限的授予或收回。
授权操作示意
此设计使得权限管理清晰简单,各子系统无需维护独立的用户、角色、权限表,只需在登录时验证门户下发的身份和权限即可。

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

加强总入口安全,限制仅公司网络或可信IP段可以访问门户。
IP白名单配置

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

记录所有关键操作以备审计。
操作日志列表

数据表设计

核心数据结构围绕用户、系统分类、系统信息、用户授权四部分设计。

1. 用户信息表:sys_user

统一存储门户用户(员工)账号信息。

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

定义系统在门户首页的分组(如后台系统、监控系统等)。

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

记录所有需要接入门户的后台系统,是门户展示的源数据。

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

记录用户与系统之间的多对多授权关系,是权限判断的依据。

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='用户与系统授权关系表';

后端接口设计

轻量化SSO的核心机制围绕两个认证中心接口展开:

  1. 创建授权码接口:门户调用,为跳转生成一次性Code。
  2. 校验授权码接口:子系统调用,用Code换取用户信息。

此设计允许各子系统保留原有登录体系,仅额外增加SSO免登能力,实现松耦合。

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

流程:用户点击门户系统卡片 → 门户调用此接口 → 认证中心生成Code并存入Redis → 返回Code给门户 → 门户拼接URL跳转。

后端核心逻辑

public Map<String, Object> createCode(HttpServletRequest request, Map<String, Object> body) {
    // 1. 验证门户登录态
    Long userId = (Long) request.getAttribute("userId");
    // 2. 校验目标系统(appId)是否存在且启用
    // 3. 校验该用户是否有权访问此系统
    // 4. 生成一次性Code,例如:String code = UUID.randomUUID().toString().replace("-", "");
    // 5. 将Code与{userId, appId}的映射存入Redis,设置短有效期(如60秒)
    // 6. 返回Code给前端
}

Code有效期短(如60秒),且使用后即删,安全性高。Redis中存储的结构示意:Key: sso:code:{code}, Value: {“userId”: 1, “appId”: “gray-center”}

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

流程:子系统获得URL中的Code → 调用此接口 → 认证中心验证Code有效性 → 返回用户基本信息。

后端核心逻辑

public Map<String, Object> verifyCode(Map<String, Object> body) {
    // 1. 获取参数:code, appId
    String code = (String) body.get("code");
    String appId = (String) body.get("appId");
    // 2. 根据code从Redis获取授权信息authInfo
    // 3. 校验authInfo是否存在、appId是否匹配、用户门户登录态是否有效
    // 4. 验证通过后,立即删除Redis中的该code记录(一次性使用)
    // 5. 根据authInfo中的userId查询用户基本信息
    // 6. 返回用户信息(如userId, username)给子系统
}

子系统获得用户信息后,便在本地生成自己的会话Token,完成免登流程。

子系统如何接入门户免登

子系统接入关键在于区分“来自门户的SSO登录”和“本地登录”,两者可并存。

1)前端处理逻辑

在路由守卫中自动检测URL参数中的code。如果存在且本地无Token,则自动向子系统后端发起SSO登录验证。

// 路由守卫示例片段
if (!token && code) {
    try {
        const res = await verifyCode(code); // 调用子系统后端验证接口
        if (res.success) {
            localStorage.setItem('token', res.data.token); // 存储子系统本地Token
            // 清除URL中的code参数,防止重复触发
            window.history.replaceState({}, '', window.location.pathname);
        }
    } catch (err) {
        // 处理错误,降级到本地登录页
    }
}
2)后端处理逻辑

子系统后端提供一个验证Code的接口,该接口负责桥接认证中心。

public Result<?> verifyCode(@RequestBody Map<String, Object> body) {
    String code = (String) body.get("code");
    String appId = "当前子系统的AppId"; // 从配置读取
    // 1. 调用认证中心 /sso/code/verify,传递code和appId
    // 2. 认证中心返回用户信息 (userId, username)
    // 3. 根据返回的用户信息,在子系统本地生成JWT Token
    Map<String, Object> claims = new HashMap<>();
    claims.put("userId", userId);
    claims.put("username", username);
    claims.put("fromSSO", true); // 关键标记:来自SSO登录
    String token = jwtUtil.generateToken(claims);
    // 4. 将本地Token和用户信息返回给前端
    return Result.ok(data);
}

关键点:在生成的JWT claims中加入fromSSO=true标记,用于后续区分登录来源。

3)为什么这样设计?

子系统保持独立登录体系,SSO作为增强能力接入。不共享Session,不共享JWT密钥,耦合度低,任何语言均可参考实现。

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

目标:用户在门户退出后,所有通过SSO方式登录的子系统应同步退出,但不影响子系统的本地登录用户。

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

当用户在门户退出时,认证中心除清理自身会话外,还需在Redis中记录一条退出标记。

public void logout(HttpServletRequest request) {
    Long userId = (Long) request.getAttribute("userId");
    // 1. 删除门户登录态:redisUtils.delete(SsoRedisKeys.userLogin(userId));
    // 2. 写入退出标记,设置较长TTL(如7天)
    long logoutTime = System.currentTimeMillis() / 1000;
    redisUtils.set(SsoRedisKeys.userLogout(userId), logoutTime, 7 * 24 * 3600);
}

Redis记录示例:Key: sso:user:logout:{userId}, Value: 退出时间戳

2)子系统拦截器:检查退出标记

子系统在鉴权拦截器中,对来自SSO的请求(即Token中fromSSO=true)额外检查门户的退出标记。

public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
    // 1. 解析Token,获取claims
    Claims claims = jwtUtil.parseToken(token);
    Long userId = claims.get("userId", Long.class);
    Boolean fromSSO = claims.get("fromSSO", Boolean.class);

    // 2. 仅对SSO登录的用户检查门户退出状态
    if (Boolean.TRUE.equals(fromSSO)) {
        String logoutKey = SsoRedisKeys.userLogout(userId);
        if (redisUtils.hasKey(logoutKey)) { // 门户已退出
            ResponseUtil.writeJson(resp, Result.fail(401, "登录已过期"));
            return false;
        }
    }
    // 3. 本地登录用户或SSO状态正常,放行
    return true;
}
3)前端响应拦截

子系统前端统一拦截401响应,自动跳转登录页。

service.interceptors.response.use(
  (response) => {
    if (response.data && response.data.code === 401) {
      localStorage.clear();
      router.push('/login');
    }
    return response;
  },
  (error) => {
    if (error.response && error.response.status === 401) {
      localStorage.clear();
      router.push('/login');
    }
    return Promise.reject(error);
  }
);

为什么必须加fromSSO标记?
用于精确控制单点退出的影响范围。只有通过门户SSO登录的用户会话会受到门户退出操作的影响,子系统自身的本地登录用户完全不受干扰,保证了子系统登录体系的独立性。

其他扩展

1. 封装SDK降低接入成本

为不同技术栈封装客户端SDK,是推动方案落地、保证一致性的关键。

  • Java (Spring Boot):可封装为starter,引入依赖并配置appIdsso-center-url即可自动注入拦截器、提供工具类。
  • Go/Node.js/PHP:提供对应的客户端库或中间件,封装Code验证、Token生成、退出检查等通用逻辑。

2. 非私有化/第三方系统接入

对于Jenkins、GitLab等无法修改代码的第三方系统,可采用轻量级方案:

  • 反向代理注入:通过Nginx等反向代理,在请求头中注入认证信息(如X-Auth-User)。
  • 模拟登录URL:门户生成一个携带临时Token的特定URL,第三方系统可识别并自动登录。
    这类方案不强制第三方系统对接SSO协议,实现成本更低。

3. 设计目的总结

本方案核心目标是:在提供统一门户和单点登录体验的同时,最大化保留各子系统的独立性,实现“统一但不绑定,增强而非重构”。通过轻量级的Code交换机制、清晰的登录来源标记(fromSSO)以及可选的SDK,使得方案能灵活适配多后台、多语言、多环境的复杂企业场景。

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

在生产环境多实例部署时,需注意以下两点:

  1. 状态集中存储:所有实例必须共享同一套状态存储(如Redis集群),确保登录态(userLogin)、授权码(code)、退出标记(userLogout)的一致性。这是实现分布式SSO的基础。
  2. 环境隔离:在Key的设计中加入环境前缀(如dev:prod:),防止不同环境的数据互相干扰。

为何选用Redis而非Session或数据库?

  • Session:与服务器实例绑定,在分布式环境下需要会话粘滞或集中存储,后者本质上等同于引入Redis,且Redis性能更优。
  • 数据库:Code生命周期极短,高频的插入和删除操作对数据库压力大、延迟高,不适合此类临时性、高并发的状态存储场景。Redis作为内存数据库,完美契合此类需求。

其他:关于子系统侵入性问题

本方案对子系统的侵入性较低,主要体现在:

  • 登录逻辑为增强而非替换:子系统原有账号密码登录方式完全保留,SSO仅是新增的一个可选登录入口。
  • Redis为轻量级依赖:仅用于存储临时Code和退出标记,与业务核心数据库隔离,不参与核心业务逻辑。
  • 改动范围集中:主要新增一个Code验证接口、一个登录来源判断以及拦截器的少量逻辑,整体接入成本可控。

这是一种“低侵入性增强”,在提供统一管理便利性的同时,充分尊重了各子系统的自治权。




上一篇:Go语言面试核心解析:并发编程、接口实现、GC机制与内存优化
下一篇:树莓派CM0部署Home Assistant实战:在512MB内存上构建智能家居控制中心
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-12 08:34 , Processed in 0.098751 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表