sa-token/sa-token-doc/sso/user-data-sync.md

243 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 用户数据同步 / 迁移
本篇文章仅提供架构设计的略微参考,真实场景中每个公司的架构设计都是千差万别的,一套设计理论未必能够适应所有公司的项目。
所以如果你觉着本篇文章的设计理念不能契合你公司的需求,请以你公司的原设计为准。
---
### 数据同步需求
在前面的不同架构 SSO 对接示例中,我们均假设了一个前提:
-- _所有的 sso-client 只负责业务操作,不存储 user 数据user 数据全部来源于 sso-server包括登录认证也都是基于 sso-server 里的 user 账号进行校验操作。_
这种架构比较简洁、清晰,是一种理想化的 SSO 架构模型。
然而更多时候,我们遇到的实际情况是:
-- _公司已经有了 N 多个系统,每个系统都有自己独立的一套账号认证体系,现在老板要让这 N 个毫无关系的系统集成单点登录。_
要完成这种需求,首先你得考虑两个问题:
1. 问题一sso-client 需不需要保留 user 数据。
- sso-client 不涉及 user 信息连表查的业务,就可以不保留 user 信息。
- sso-client 涉及 user 信息连表查业务,就需要在 sso-client 保留 user 数据。
2. 问题二:如果保留的话,是和 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 | email |
| :-------- | :-------- | :-------- | :-------- | :-------- | :-------- |
| 10001 | ... | ... | ... | ... | ... |
| 10002 | 小明 | cat.jpg | 123456 | 18 | `23397@xx.com` |
| 10003 | ... | ... | ... | ... | ... |
在电商系统里中,张三的数据库信息为:
| id | name | avatar | money | email |
| :-------- | :-------- | :-------- | :-------- | :-------- |
| 100334 | ... | ... | ... | ... |
| 100335 | 二明 | dog.jpg | 1000 | `23397@xx.com` |
| 100336 | ... | ... | ... | ... |
这里的关键点在于,虽然用户 “张三” 在每个系统里的资料都是不同的,但是程序要想办法将它们识别为同一个用户,
要做到这一点,就需要我们准备一个关键字段将信息打通串联起来。例如表中的 “邮箱” 信息可以作为这个“关联字段”。
(注:此处仅展示使用邮箱作为关联字段的操作,实际上除了邮箱以外,手机号、身份证号等具有唯一性的信息都可以作为关联字段)
首先,在 sso-server 端,我们需要重写一下 `checkTicketAppendData` 函数,使其在 “校验 ticket 返回 loginId” 时,追加返回 email 字段。
``` java
// 配置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 信息并登录:
``` java
// 配置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` 函数即可:
``` java
// 配置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 对这个用户账号的完整处理步骤是怎样的?]
> 1. 用户进入 sso-client 登录页面,点击上面的 [ 使用 xx 认证中心快捷登录 ] 按钮,浏览器跳转至 sso-server 认证中心。
> 2. 如果用户在 sso-server 有账号,则直接登录,如果没有,则注册账号并登录。
> 3. sso-server 重定向回 sso-client 端,并携带 ticket 参数。
> 4. sso-client 获取 ticket 参数,并解析出 center_id 值。
> 5. 根据 center_id 从 user 表查数据:
> - 5.1 查的到,证明有账号,直接登录。
> - 5.2 查不到,证明无账号,程序自动给他添加一条 user 账号,并登录。
> 6. 登录完成。