sso单点登录的权限变更同步的三种核心方案(实时同步、半实时同步、被动同步)
一、实时同步:权限变更时主动通知子应用
核心逻辑:权限一旦变更,立即通过 “主动推送” 通知相关子应用,子应用实时更新本地权限数据。适用场景:紧急权限变更(如用户离职被移除所有权限、临时禁止访问敏感系统),要求 “立即生效”。
1. 实现流程
以 “管理员在权限中心移除用户 A 对「财务系统」的「审批权限」” 为例:
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 分钟后生效” 为例:
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 尝试审批财务单据,此时权限已被移除” 为例:
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. 解决方案
-
针对 “子应用下线 / 网络中断” :
- 权限中心实现 “持久化重试队列”(如用 Redis 或数据库存储待推送任务,而非内存队列),子应用恢复后自动重试。
- 子应用启动时主动 “拉取全量权限”(如调用
https://permission.example.com/full-sync?appId=finance-app),补充遗漏的同步。
-
针对 “请求篡改 / 伪造” :
-
所有推送请求必须加签名校验(如用权限中心的私钥对请求体签名,子应用用公钥验签),示例:
// 权限中心签名 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; -
推送接口启用 HTTPS,防止中间人攻击窃取请求内容。
-
-
针对 “重试机制失效” :
- 重试队列添加 “告警机制”(如重试超过 3 次未成功,触发短信 / 邮件告警给运维)。
- 每日凌晨执行 “全量权限比对”(权限中心与子应用对账),发现差异后自动同步。
二、半实时同步:极端失效场景与应对
半实时同步依赖 “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 未过期的延迟风险” :
- 缩短 Token 有效期(如从 30 分钟改为 5 分钟),减少越权窗口;
- 关键操作叠加 “被动同步”(如用户点击 “审批” 时,即使 Token 未过期,也临时校验最新权限),兜底延迟风险。
-
针对 “refreshToken 失效 / 被窃取” :
- refreshToken 存储在 HttpOnly + Secure Cookie 中(禁止前端 JS 访问),防止 XSS 攻击窃取;
- 实现 “refreshToken 单设备登录”(用户在新设备登录时,旧设备的 refreshToken 立即失效),防止多设备泄露;
- 给 refreshToken 加 “设备标识”(如浏览器 UA、IP 段),异常设备使用时触发二次验证(如短信验证码)。
-
针对 “SSO / 权限中心故障” :
- 子应用实现 “Token 降级策略”:若 SSO 故障,临时延长 Token 有效期(如额外延长 1 小时),并提示 “当前系统维护,部分功能受限”;
- SSO / 权限中心部署多实例集群,避免单点故障。
三、被动同步:极端失效场景与应对
被动同步的核心依赖 “操作时实时调用权限中心校验”,极端场景下会因 “权限中心不可用” 或 “校验结果被篡改” 失效。
1. 极端失效场景
(1)权限中心服务崩溃 / 网络中断
-
场景:用户执行 “删除订单” 操作时,子应用调用权限中心校验接口,但权限中心因服务器宕机、网络中断无法响应。
-
后果:子应用无法判断用户是否有权限,可能出现两种极端情况:
- 拒绝操作:正常用户无法使用关键功能(如客服无法删除无效订单);
- 允许操作:存在越权风险(如普通用户删除订单)。
(2)校验接口超时导致用户体验差
- 场景:权限中心因高并发(如秒杀活动期间大量校验请求)导致接口响应延迟(超过 5 秒)。
- 后果:用户点击操作后长时间等待,体验崩溃,甚至重复点击导致系统异常。
(3)校验结果被中间人篡改
- 场景:攻击者拦截子应用与权限中心的校验请求,将 “不允许”(
allowed: false)改为 “允许”(allowed: true)。 - 后果:用户越权执行敏感操作(如删除全量订单)。
2. 解决方案
-
针对 “权限中心不可用” :
- 实现 “降级熔断” 策略:若权限中心连续 3 次超时 / 报错,自动触发降级 —— 允许 “已缓存过的合法权限” 继续操作(如 10 秒内校验过的用户),拒绝新用户操作,并提示 “系统临时维护”;
- 权限中心部署异地多活集群(如北京、上海机房各部署一套),子应用优先调用本地机房接口,本地故障时自动切换异地接口。
-
针对 “校验接口超时” :
- 给校验接口设置短超时时间(如 2 秒),超时后触发降级;
- 加本地缓存(如 Redis),缓存 10 秒内的校验结果(同一用户同一操作,10 秒内不重复调用权限中心),减少请求量。
-
针对 “校验结果被篡改” :
-
校验接口启用 HTTPS,防止中间人窃听和篡改;
-
权限中心返回校验结果时附带数字签名(如用私钥签名),子应用验签通过后才认可结果,示例:
// 权限中心返回结果 const result = { allowed: true, sign: 'xxx' }; // sign 是对 { allowed: true } 的签名 // 子应用验签 const valid = verifySign(result.allowed, result.sign); if (!valid) { throw new Error('校验结果无效'); }
-
四、通用防失效原则(所有方案都适用)
- 避免单点故障:权限中心、SSO 中心、子应用均部署多实例集群,网络用多链路冗余(如电信 + 联通光缆)。
- 关键操作日志审计:所有权限变更、权限校验操作记录详细日志(用户 ID、操作时间、权限内容、IP 地址),即使失效也能追溯问题。
- 定期演练故障恢复:每月模拟 “权限中心崩溃”“网络中断” 等场景,测试降级策略是否生效,避免实战时手忙脚乱。
总结(没有绝对安全,但有绝对防御)
没有任何方案能 100% 避免极端失效,但通过 “冗余设计(多实例 / 多链路)+ 降级策略(故障时兜底)+ 安全校验(防篡改) ”,可以将失效概率降到极低,且即使失效也能最小化损失。