单点登录中权限同步的解决方案及验证策略

0 阅读12分钟

sso单点登录的权限变更同步的三种核心方案(实时同步、半实时同步、被动同步)

一、实时同步:权限变更时主动通知子应用

核心逻辑:权限一旦变更,立即通过 “主动推送” 通知相关子应用,子应用实时更新本地权限数据。适用场景:紧急权限变更(如用户离职被移除所有权限、临时禁止访问敏感系统),要求 “立即生效”。

1. 实现流程

以 “管理员在权限中心移除用户 A 对「财务系统」的「审批权限」” 为例:

exported_image.png

2. 关键技术:WebHook 回调
  • 权限中心配置:提前录入各子应用的 “权限同步接口”(WebHook 地址),例如财务系统的接口为 https://finance-app.example.com/api/permission/sync
  • 推送格式:权限中心向子应用接口发送 POST 请求,携带用户 ID、应用 ID、最新权限列表。

权限中心推送代码示例(Node.js)

// 权限中心:当权限变更时触发
async function onPermissionChange(userId, appId, newPermissions) {
  // 1. 先更新权限中心数据库(省略)
  // 2. 获取子应用的 WebHook 地址(从配置中读取)
  const webhookUrl = getAppWebhook(appId); // 如 "https://finance-app.example.com/api/permission/sync"
  // 3. 向子应用推送最新权限
  try {
    await fetch(webhookUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        userId: userId, // "user123"
        appId: appId,   // "finance-app"
        permissions: newPermissions, // ["view", "export"](移除了"approve")
        timestamp: Date.now(),
        sign: generateSign(newPermissions) // 签名,防止篡改
      })
    });
    console.log(`向${appId}同步权限成功`);
  } catch (err) {
    // 失败重试机制(如存入消息队列,5分钟后重试)
    addToRetryQueue({ userId, appId, newPermissions });
    console.error(`同步失败,已加入重试队列:${err.message}`);
  }
}

子应用接收代码示例(财务系统,Node.js/Express)

// 财务系统:接收权限同步的接口
app.post('/api/permission/sync', async (req, res) => {
  const { userId, permissions, sign } = req.body;
  // 1. 验证签名(防止伪造请求)
  if (!verifySign(permissions, sign)) {
    return res.status(403).send('签名无效');
  }
  // 2. 更新本地缓存(如 Redis)中用户的权限
  await redisClient.set(
    `finance:permission:${userId}`, 
    JSON.stringify(permissions), 
    'EX', 
    86400 // 缓存1天
  );
  // 3. (可选)如果用户在线,强制刷新其页面权限
  pushToUserSocket(userId, { type: 'permissionUpdate', permissions });
  res.send({ code: 0, msg: '同步成功' });
});
3. 注意
  • 优点:实时性 100%,权限变更后子应用立即生效。

  • 注意事项

    • 必须实现 “重试机制”(如消息队列),防止子应用临时下线导致同步失败。
    • 接口需加签名验证,防止恶意请求篡改权限。

二、半实时同步:本地凭证过期时同步

核心逻辑:子应用的本地凭证(如 Token)设置短期有效期,过期后需向 SSO / 权限中心 “刷新凭证”,此时获取最新权限。适用场景:非紧急权限变更(如新增普通操作权限),可接受 5-30 分钟延迟。

1. 实现流程

以 “用户 A 的「财务系统」权限新增了「导出报表」权限,10 分钟后生效” 为例:

image.png

2. 关键技术:短期 Token + 刷新机制
  • 本地凭证设计:子应用的 Token 包含过期时间(如 10 分钟)和刷新令牌(refreshToken,有效期 7 天)。
  • 刷新流程:Token 过期后,用 refreshToken 向 SSO 中心换取新 Token,同时获取最新权限。

子应用前端代码示例(Vue)

// 财务系统前端:请求拦截器,处理Token过期
axios.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    // 如果是401(Token过期)且未重试过
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      try {
        // 1. 用refreshToken向SSO中心刷新凭证
        const { data } = await axios.post('https://sso.example.com/refresh', {
          refreshToken: localStorage.getItem('finance_refreshToken'),
          appId: 'finance-app'
        });
        // 2. 保存新Token和权限(包含新增的"export")
        localStorage.setItem('finance_token', data.newToken);
        localStorage.setItem('finance_permissions', JSON.stringify(data.permissions));
        // 3. 用新Token重试原请求
        originalRequest.headers.Authorization = `Bearer ${data.newToken}`;
        return axios(originalRequest);
      } catch (err) {
        // 刷新失败(如refreshToken过期),强制跳转登录
        localStorage.removeItem('finance_token');
        window.location.href = 'https://sso.example.com/login?redirect=https://finance-app.example.com';
      }
    }
    return Promise.reject(error);
  }
);

SSO 中心刷新接口代码示例(Node.js)

// SSO中心:处理子应用的Token刷新请求
app.post('/refresh', async (req, res) => {
  const { refreshToken, appId } = req.body;
  // 1. 验证refreshToken有效性(从数据库/Redis查询)
  const user = await verifyRefreshToken(refreshToken);
  if (!user) {
    return res.status(401).send('refreshToken无效');
  }
  // 2. 向权限中心查询该用户在子应用的最新权限
  const permissions = await permissionCenter.getPermissions(user.id, appId);
  // 3. 生成新的子应用Token(包含权限)
  const newToken = jwt.sign(
    { 
      userId: user.id, 
      appId: appId, 
      permissions: permissions, // ["view", "export"]
      exp: Math.floor(Date.now() / 1000) + 600 // 10分钟后过期
    },
    'finance_app_secret' // 子应用专属密钥
  );
  res.send({
    newToken: newToken,
    permissions: permissions,
    refreshToken: refreshToken // 可复用旧refreshToken,或生成新的
  });
});
3. 注意
  • 优点:实现简单,无需主动推送,子应用和权限中心耦合低。

  • 注意

    • Token 有效期需合理设置(太短影响体验,太长延迟高,推荐 10-30 分钟)。
    • 刷新令牌(refreshToken)需妥善保管(如存在 HttpOnly Cookie),防止泄露。

三、被动同步:关键操作时校验最新权限

核心逻辑:子应用在执行敏感操作(如删除数据、审批)时,不依赖本地缓存,临时向权限中心查询最新权限。适用场景:高安全级别操作(如财务审批、订单删除),必须确保权限是 “当前最新”。

1. 实现流程

以 “用户 A 尝试审批财务单据,此时权限已被移除” 为例:

image.png

2. 关键技术:实时校验接口

子应用在敏感操作的后端接口中,同步调用权限中心的 “权限校验接口”,确保结果实时。

财务系统后端代码示例(审批接口)

// 财务系统:审批单据接口(敏感操作)
app.post('/api/approve-bill', async (req, res) => {
  const { billId } = req.body;
  const userId = req.user.id; // 从本地Token中解析用户ID
  // 1. 被动同步:向权限中心校验最新权限
  const hasPermission = await checkPermission(userId, 'finance-app', 'approve');
  if (!hasPermission) {
    return res.status(403).send('无审批权限,请联系管理员');
  }
  // 2. 权限通过,执行审批逻辑(省略)
  await billService.approve(billId, userId);
  res.send({ code: 0, msg: '审批成功' });
});

// 调用权限中心校验的函数
async function checkPermission(userId, appId, action) {
  const response = await fetch(
    `https://permission.example.com/check?userId=${userId}&appId=${appId}&action=${action}`,
    { headers: { 'Authorization': 'SSO_TOKEN' } } // 子应用在SSO的身份凭证
  );
  const data = await response.json();
  return data.allowed; // true/false
}

权限中心校验接口代码示例

// 权限中心:校验用户是否有某个操作的权限
app.get('/check', async (req, res) => {
  const { userId, appId, action } = req.query;
  // 1. 从数据库查询用户在该应用的最新权限
  const userPermissions = await db.query(
    'SELECT permissions FROM user_app_permissions WHERE user_id = ? AND app_id = ?',
    [userId, appId]
  );
  // 2. 判断是否包含目标操作权限
  const allowed = userPermissions.length > 0 
    && userPermissions[0].permissions.includes(action);
  res.send({ allowed: allowed });
});
3. 注意
  • 优点:安全性最高,确保敏感操作的权限一定是最新的。

  • 注意事项

    • 会增加接口调用次数,可能影响性能(可加缓存,但需设置极短过期时间,如 10 秒)。
    • 仅用于关键操作,避免所有接口都走被动同步(否则性能损耗过大)。

三种方案的核心差异和选择依据:

方案实时性实现复杂度适用场景典型举例
实时同步立即生效中(需推送 + 重试)紧急权限移除、用户离职禁止访问财务系统
半实时同步延迟 5-30 分钟低(依赖 Token 过期)新增普通权限、权限微调增加 “导出报表” 权限
被动同步操作时实时低(接口校验)高敏感操作(审批、删除)财务单据审批、订单删除

实际项目中通常 “组合使用”:用半实时同步覆盖大部分场景,实时同步处理紧急情况,被动同步兜底敏感操作,兼顾效率和安全性。

极端情况下导致的失效

这三种同步方案在极端场景下确实可能失效,核心原因通常是 “网络异常”“系统故障” 或 “设计漏洞”

一、实时同步:极端失效场景与应对

实时同步的核心依赖 “权限中心主动推送 → 子应用接收处理” 的链路,任何一个环节断裂都会导致失效。

1. 极端失效场景
(1)子应用服务临时下线 / 网络中断
  • 场景:权限中心推送权限变更时,子应用刚好在重启(如发布新版本),或子应用与权限中心之间的网络中断(如机房光缆故障)。
  • 后果:子应用未收到同步请求,权限变更未生效(例如用户已被移除 “审批权限”,但子应用仍保留旧权限,用户可继续审批)。
(2)推送请求被篡改 / 伪造
  • 场景:攻击者拦截权限中心的推送请求,篡改内容(如给普通用户添加 “管理员权限”),或伪造推送请求(冒充权限中心发送虚假权限)。
  • 后果:子应用执行错误的权限更新,导致权限泄露或越权操作。
(3)重试机制失效
  • 场景:权限中心的重试队列(如 Kafka)因磁盘满、服务崩溃等原因无法工作,推送失败后无法重试。
  • 后果:权限变更彻底丢失,子应用长期使用旧权限。
2. 解决方案
  • 针对 “子应用下线 / 网络中断”

    1. 权限中心实现 “持久化重试队列”(如用 Redis 或数据库存储待推送任务,而非内存队列),子应用恢复后自动重试。
    2. 子应用启动时主动 “拉取全量权限”(如调用 https://permission.example.com/full-sync?appId=finance-app),补充遗漏的同步。
  • 针对 “请求篡改 / 伪造”

    1. 所有推送请求必须加签名校验(如用权限中心的私钥对请求体签名,子应用用公钥验签),示例:

      // 权限中心签名
      const sign = crypto.createHmac('sha256', PRIVATE_KEY)
        .update(JSON.stringify(reqBody))
        .digest('hex');
      // 子应用验签
      const valid = crypto.createHmac('sha256', PUBLIC_KEY)
        .update(JSON.stringify(reqBody))
        .digest('hex') === reqSign;
      
    2. 推送接口启用 HTTPS,防止中间人攻击窃取请求内容。

  • 针对 “重试机制失效”

    1. 重试队列添加 “告警机制”(如重试超过 3 次未成功,触发短信 / 邮件告警给运维)。
    2. 每日凌晨执行 “全量权限比对”(权限中心与子应用对账),发现差异后自动同步。

二、半实时同步:极端失效场景与应对

半实时同步依赖 “Token 过期 → 刷新获取新权限” 的链路,极端场景下会因 “Token 未过期” 或 “刷新失败” 导致失效。

1. 极端失效场景
(1)Token 未过期,权限已变更(延迟窗口期内的风险)
  • 场景:子应用 Token 有效期设为 30 分钟,用户 A 的 “审批权限” 在 Token 生成后 10 分钟被移除,但 Token 未过期,用户仍能使用旧权限。
  • 后果:权限变更延迟 20 分钟生效,期间用户可越权操作(如继续审批单据)。
(2)refreshToken 失效 / 被窃取
  • 场景:用户的 refreshToken 因过期(如 7 天有效期到了)或被攻击者窃取,导致 Token 过期后无法刷新,或攻击者用窃取的 refreshToken 获取新权限。

  • 后果

    • 正常用户:Token 过期后被强制登出,体验差;
    • 攻击者:可能用窃取的 refreshToken 长期获取权限。
(3)SSO / 权限中心故障,刷新失败
  • 场景:Token 过期时,SSO 中心或权限中心因服务器崩溃、数据库故障无法提供刷新服务。
  • 后果:所有用户无法刷新 Token,被强制登出,子应用无法使用。
2. 解决方案
  • 针对 “Token 未过期的延迟风险”

    1. 缩短 Token 有效期(如从 30 分钟改为 5 分钟),减少越权窗口;
    2. 关键操作叠加 “被动同步”(如用户点击 “审批” 时,即使 Token 未过期,也临时校验最新权限),兜底延迟风险。
  • 针对 “refreshToken 失效 / 被窃取”

    1. refreshToken 存储在 HttpOnly + Secure Cookie 中(禁止前端 JS 访问),防止 XSS 攻击窃取;
    2. 实现 “refreshToken 单设备登录”(用户在新设备登录时,旧设备的 refreshToken 立即失效),防止多设备泄露;
    3. 给 refreshToken 加 “设备标识”(如浏览器 UA、IP 段),异常设备使用时触发二次验证(如短信验证码)。
  • 针对 “SSO / 权限中心故障”

    1. 子应用实现 “Token 降级策略”:若 SSO 故障,临时延长 Token 有效期(如额外延长 1 小时),并提示 “当前系统维护,部分功能受限”;
    2. SSO / 权限中心部署多实例集群,避免单点故障。

三、被动同步:极端失效场景与应对

被动同步的核心依赖 “操作时实时调用权限中心校验”,极端场景下会因 “权限中心不可用” 或 “校验结果被篡改” 失效。

1. 极端失效场景
(1)权限中心服务崩溃 / 网络中断
  • 场景:用户执行 “删除订单” 操作时,子应用调用权限中心校验接口,但权限中心因服务器宕机、网络中断无法响应。

  • 后果:子应用无法判断用户是否有权限,可能出现两种极端情况:

    • 拒绝操作:正常用户无法使用关键功能(如客服无法删除无效订单);
    • 允许操作:存在越权风险(如普通用户删除订单)。
(2)校验接口超时导致用户体验差
  • 场景:权限中心因高并发(如秒杀活动期间大量校验请求)导致接口响应延迟(超过 5 秒)。
  • 后果:用户点击操作后长时间等待,体验崩溃,甚至重复点击导致系统异常。
(3)校验结果被中间人篡改
  • 场景:攻击者拦截子应用与权限中心的校验请求,将 “不允许”(allowed: false)改为 “允许”(allowed: true)。
  • 后果:用户越权执行敏感操作(如删除全量订单)。
2. 解决方案
  • 针对 “权限中心不可用”

    1. 实现 “降级熔断” 策略:若权限中心连续 3 次超时 / 报错,自动触发降级 —— 允许 “已缓存过的合法权限” 继续操作(如 10 秒内校验过的用户),拒绝新用户操作,并提示 “系统临时维护”;
    2. 权限中心部署异地多活集群(如北京、上海机房各部署一套),子应用优先调用本地机房接口,本地故障时自动切换异地接口。
  • 针对 “校验接口超时”

    1. 给校验接口设置短超时时间(如 2 秒),超时后触发降级;
    2. 加本地缓存(如 Redis),缓存 10 秒内的校验结果(同一用户同一操作,10 秒内不重复调用权限中心),减少请求量。
  • 针对 “校验结果被篡改”

    1. 校验接口启用 HTTPS,防止中间人窃听和篡改;

    2. 权限中心返回校验结果时附带数字签名(如用私钥签名),子应用验签通过后才认可结果,示例:

      // 权限中心返回结果
      const result = { allowed: true, sign: 'xxx' }; // sign 是对 { allowed: true } 的签名
      // 子应用验签
      const valid = verifySign(result.allowed, result.sign);
      if (!valid) { throw new Error('校验结果无效'); }
      

四、通用防失效原则(所有方案都适用)

  1. 避免单点故障:权限中心、SSO 中心、子应用均部署多实例集群,网络用多链路冗余(如电信 + 联通光缆)。
  2. 关键操作日志审计:所有权限变更、权限校验操作记录详细日志(用户 ID、操作时间、权限内容、IP 地址),即使失效也能追溯问题。
  3. 定期演练故障恢复:每月模拟 “权限中心崩溃”“网络中断” 等场景,测试降级策略是否生效,避免实战时手忙脚乱。

总结(没有绝对安全,但有绝对防御)

没有任何方案能 100% 避免极端失效,但通过 “冗余设计(多实例 / 多链路)+ 降级策略(故障时兜底)+ 安全校验(防篡改)  ”,可以将失效概率降到极低,且即使失效也能最小化损失。