12 KiB
用户数据同步 / 迁移
本篇文章仅提供架构设计的略微参考,真实场景中每个公司的架构设计都是千差万别的,一套设计理论未必能够适应所有公司的项目。 所以如果你觉着本篇文章的设计理念不能契合你公司的需求,请以你公司的原设计为准。
数据同步需求
在前面的不同架构 SSO 对接示例中,我们均假设了一个前提:
-- 所有的 sso-client 只负责业务操作,不存储 user 数据,user 数据全部来源于 sso-server,包括登录认证也都是基于 sso-server 里的 user 账号进行校验操作。
这种架构比较简洁、清晰,是一种理想化的 SSO 架构模型。
然而更多时候,我们遇到的实际情况是:
-- 公司已经有了 N 多个系统,每个系统都有自己独立的一套账号认证体系,现在老板要让这 N 个毫无关系的系统集成单点登录。
要完成这种需求,首先你得考虑两个问题:
- 问题一:sso-client 需不需要保留 user 数据。
- sso-client 不涉及 user 信息连表查的业务,就可以不保留 user 信息。
- sso-client 涉及 user 信息连表查业务,就需要在 sso-client 保留 user 数据。
- 问题二:如果保留的话,是和 sso-server 强同步,还是弱同步。
- 强同步就是指 sso-client 的 user 数据和 sso-server 的 user 数据,字段值必须保持一致。比如说:一个用户在server端昵称修改为“张三”,那么在 client 端也要实时同步修改。
- 弱同步就是指两边可以各改各的。比如说:一个用户 server 端修改了昵称为 “张三”,他在 client 端依然可以昵称为 “李四”。
由此可大致分为三种设计方案:
方案序号 | 方案名称 | 简单说明 | 适用系统 |
---|---|---|---|
方案一 | 统一迁移 | 统一把用户数据迁移到 sso-server 认证中心再进行对接 | 比较简单的系统,业务上不需要 user 信息连表查 |
方案二 | 实时同步 | 按照一定的规则,使 sso-client 和 sso-server 保持 user 信息实时同步 | 一般业务上需要 user 信息连表查的系统 |
方案三 | 字段关联 | 不同步,但找一个关键字段,将 sso-client 和 sso-server 的 user 账号进行关联起来 | sso-client 不打算过分依赖 sso-server 的 user 数据,只是想借助 sso-server 完成一下统一登录 |
下面逐一拆解三种方案具体实现。
1、方案一:统一迁移
对接工作开发前,sso-client 的 user 数据完全迁移到 sso-server 中,且自身不再保留 user 数据,只进行业务数据处理操作。
这种方案其实不必过多讲解,因为数据完成迁移后整个架构就转化为了上述的“理想化SSO模型”,后续对接也比较方便。 迁移方式可以选择数据库同步工具,或者手写代码从 sso-client 库读取数据然后 insert 到 sso-server 库中。 这并非此文探讨的重点,因此不再过多赘述了。
- 方案优点:架构简洁明了,SSO 登录、注销对接起来非常方便
- 方案缺点:sso-client 不存储 user 信息,因此业务上需要连表查询 user 信息的地方会比较麻烦(例如:拉取帖子列表时需要附加显示用户头像和昵称信息)
方案适用范围:适合业务比较简单,不涉及 user资料连表查业务 的子系统。
2、方案二:实时同步
首先,对接前,数据还是要迁移的,只不过迁移后 sso-client 的 user 数据不删除掉,依然保留。
然后在项目运行阶段,每当 sso-server 的 user 数据发生变动时(增删改),逐一向每个 sso-client 推送变化信息。使 sso-client 与 sso-server 的 user 数据保持强同步。
你可能会有疑问,那 sso-client 的 user 数据发生变动时,要不要向 sso-server 推送信息,我的建议是:尽量不要让 sso-client 的 user 信息主动发生变化。
举个例子:
公司有电商、论坛、短视频 3 个子系统 + 1 个 sso-server 认证中心,无论用户从哪个子系统点击 “修改我的资料” 按钮时,都应该统一跳转到 sso-server 认证中心进行修改, 修改完毕后再由 sso-server 将 user 信息推送至 3 个子系统。以此来保证 4 个系统间的 user 信息同步。
- 方案优点:sso-client 存储了 user 信息,可以比较方便的进行 user 连表查操作。
- 方案缺点:sso-server 与 sso-client 的 user 数据同步功能不算简单,开发起来可能要耗费一段不小的工期。
方案适用范围:一般业务上需要 user 信息连表查的子系统都适合。
3、方案三:字段关联
如果子系统不需要和 sso-server 做到信息强同步,可以使用字段关联法做到账户关联进行登录。
举个例子:公司有三个子系统,电商、论坛、短视频。同一个用户可以在这三个子系统以及 sso-server 认证中心拥有不同的昵称、头像等信息,互不干扰。
例如,在 sso-server 认证中心里,张三的数据库信息为:
id | username | avatar | password | age | |
---|---|---|---|---|---|
10001 | ... | ... | ... | ... | ... |
10002 | 小明 | cat.jpg | 123456 | 18 | 23397@xx.com |
10003 | ... | ... | ... | ... | ... |
在电商系统里中,张三的数据库信息为:
id | name | avatar | money | |
---|---|---|---|---|
100334 | ... | ... | ... | ... |
100335 | 二明 | dog.jpg | 1000 | 23397@xx.com |
100336 | ... | ... | ... | ... |
这里的关键点在于,虽然用户 “张三” 在每个系统里的资料都是不同的,但是程序要想办法将它们识别为同一个用户, 要做到这一点,就需要我们准备一个关键字段将信息打通串联起来。例如表中的 “邮箱” 信息可以作为这个“关联字段”。
(注:此处仅展示使用邮箱作为关联字段的操作,实际上除了邮箱以外,手机号、身份证号等具有唯一性的信息都可以作为关联字段)
首先,在 sso-server 端,我们需要重写一下 checkTicketAppendData
函数,使其在 “校验 ticket 返回 loginId” 时,追加返回 email 字段。
// 配置SSO相关参数
@Autowired
private void configSso(SaSsoServerConfig ssoServer) {
// 其它配置 ...
// 配置:Ticket校验函数
ssoServer.checkTicketAppendData = (loginId, result) -> {
System.out.println("-------- 追加返回信息到 sso-client --------");
// 在校验 ticket 后,给 sso-client 端追加返回信息的函数
SysUser user = sysUserMapper.getById(loginId);
result.set("email", user.getEmail());
// result.set("user", user); // 你也可以将整个user 对象的信息都返回到 sso-client,自由决定
return result;
};
}
在 sso-client 端,重写 ticketResultHandle 函数,根据 sso-server 返回的信息查询本地 user 信息并登录:
// 配置SSO相关参数
@Autowired
private void configSso(SaSsoClientConfig ssoClient) {
// 其它配置 ...
// 自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用)
ssoClient.ticketResultHandle = (ctr, back) -> {
System.out.println("--------- 自定义 ticket 校验结果处理函数 ---------");
System.out.println("此账号在 sso-server 的 userId:" + ctr.loginId);
System.out.println("此账号在 sso-server 会话剩余有效期:" + ctr.remainSessionTimeout + " 秒");
System.out.println("此账号返回的 email 信息:" + ctr.result.get("email"));
// 模拟代码:
// 根据 email 字段找到此账号在本系统对应的 user 信息
String email = (String) ctr.result.get("email");
SysUser user = sysUserMapper.getByEmail(email);
// 如果找不到,说明是首次登录本系统的新用户,需要自动注册一个新账号给他
if(user == null) {
// 涉及到数据库操作,此处仅做模拟代码
// 1、构建 user 信息
// 2、插入到数据库
// 3、查询出最新刚插入的这条 user 信息
user = sysUserMapper.getByEmail(email);
}
// 进行登录
StpUtil.login(user.getId(), ctr.remainSessionTimeout);
StpUtil.getSession().set("user", user);
// 一切工作完毕,重定向回 back 页面
return SaHolder.getResponse().redirect(back);
};
}
至此完毕。
- 方案优点:
- 1、sso-client 不需要和 sso-server 保持信息强同步,实现起来不复杂,架构也比较清晰易维护。
- 2、同一个用户的信息,sso-client 可以和 sso-client 保持不同,各自维护各自的,互不干扰。
- 方案缺点:好像没啥缺点,除非你觉着上述的第2条优点属于缺点。
方案适用范围:在 user 信息方面不打算过分依赖 sso-server 的系统,希望自己维护自己的 user 信息,只是想借助 sso-server 完成一下统一登录。
4、扩展:没有关联字段
如果我们的子系统 user 表没有邮箱、手机号等唯一性字段和 sso-server 的 user 表进行关联,该怎么办呢?
没有字段,那就创造个字段,例如:
id | name | avatar | age | center_id |
---|---|---|---|---|
205421 | ... | ... | ... | ... |
205422 | 小风筝 | dog.jpg | 21 | 10002 |
205423 | ... | ... | ... | ... |
如上表所示,我们可以在子系统的 user 表新增一列 center_id
,记录这个用户在认证中心所属的账号id。然后在登录时根据这个 center_id
来查找相应的用户。
由于 sso-server 端默认就是会返回 loginId 参数的,因此在 sso-server 端不必再重写一下 checkTicketAppendData
函数来追加返回信息了,
我们只需要重写 sso-client 端的 ticketResultHandle
函数即可:
// 配置SSO相关参数
@Autowired
private void configSso(SaSsoClientConfig ssoClient) {
// 其它配置 ...
// 自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用)
ssoClient.ticketResultHandle = (ctr, back) -> {
System.out.println("--------- 自定义 ticket 校验结果处理函数 ---------");
System.out.println("此账号在 sso-server 的 userId:" + ctr.loginId);
System.out.println("此账号在 sso-server 会话剩余有效期:" + ctr.remainSessionTimeout + " 秒");
// 模拟代码:
// 根据 center_id 字段找到此账号在本系统对应的 user 信息
long centerId = SaFoxUtil.getValueByType(ctr.loginId, long.class);
SysUser user = sysUserMapper.getByCenterId(centerId);
// 如果找不到,说明是首次登录本系统的新用户,需要自动注册一个新账号给他
if(user == null) {
// 涉及到数据库操作,此处仅做模拟
// 1、构建 user 信息
// 2、插入到数据库
// 3、查询出最新刚插入的这条 user 信息
user = sysUserMapper.getByCenterId(userId);
}
// 进行登录
// 注意此处需要使用 centerId 进行登录,否则该账号将无法正常完成单点注销功能
StpUtil.login(centerId, ctr.remainSessionTimeout);
StpUtil.getSession().set("user", user);
// 一切工作完毕,重定向回 back 页面
return SaHolder.getResponse().redirect(back);
};
}
至此完毕。
[!INFO| label:提问:按照方案三,一个用户登录过程中,sso-server 和 sso-client 对这个用户账号的完整处理步骤是怎样的?]
- 用户进入 sso-client 登录页面,点击上面的 [ 使用 xx 认证中心快捷登录 ] 按钮,浏览器跳转至 sso-server 认证中心。
- 如果用户在 sso-server 有账号,则直接登录,如果没有,则注册账号并登录。
- sso-server 重定向回 sso-client 端,并携带 ticket 参数。
- sso-client 获取 ticket 参数,并解析出 center_id 值。
- 根据 center_id 从 user 表查数据: - 5.1 查的到,证明有账号,直接登录。 - 5.2 查不到,证明无账号,程序自动给他添加一条 user 账号,并登录。
- 登录完成。