diff --git a/README.md b/README.md index 97ea985e..4848d217 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ Sa-Token SSO 分为三种模式,解决同域、跨域、共享Redis、跨Redis 6. 提供前后端分离整合方案:无论是 sso-server 还是 sso-client 的前后端分离都可以整合。 7. 提供安全校验:域名校验、ticket校验、参数签名校验,有效防 ticket 劫持,防请求重放等攻击。 8. 参数防丢:笔者曾试验多个SSO框架,均有参数丢失情况,比如登录前是:`http://a.com?id=1&name=2`,登录成功后就变成了:`http://a.com?id=1`,Sa-Token-SSO 内有专门算法保证了参数不丢失,登录成功后精准原路返回。 -9. 提供用户数据迁移方案的建议:开发前统一迁移、运行时实时数据同步、根据关键字段匹配、根据 center_id 字段匹配。 +9. 提供用户数据同步/迁移方案的建议:开发前统一迁移、运行时实时数据同步、根据关联字段匹配、根据 center_id 字段匹配等。 10. 提供直接可运行的 demo 示例,帮助你快速熟悉 SSO 大致登录流程。 diff --git a/sa-token-doc/_sidebar.md b/sa-token-doc/_sidebar.md index 230e3f31..c75611db 100644 --- a/sa-token-doc/_sidebar.md +++ b/sa-token-doc/_sidebar.md @@ -47,6 +47,7 @@ - [前后端分离下的整合方案](/sso/sso-h5) - [平台中心跳转模式](/sso/sso-home-jump) - [不同 Client 不同秘钥](/sso/sso-diff-key) + - [用户数据同步 / 迁移](/sso/user-data-sync) - [常见问题总结](/sso/sso-questions) - [Sa-Sso-Pro:单点登录商业版](/sso/sso-pro) diff --git a/sa-token-doc/more/update-log.md b/sa-token-doc/more/update-log.md index 231d1c32..ba48a84e 100644 --- a/sa-token-doc/more/update-log.md +++ b/sa-token-doc/more/update-log.md @@ -31,6 +31,8 @@ - 重构:匿名 client 将不再能解析出所有应用的 ticket。**[不向下兼容]** - 新增:新增 homeRoute 配置项:在 /sso/auth 登录后不指定 redirect 参数的情况下默认跳转的路由。 - 优化:优化登录有效期策略,SSO Client 端登录时将延续 SSO Server 端的会话剩余有效期。 + - 新增:新增 `checkTicketAppendData` 策略函数,用于在校验 ticket 后,给 sso-client 端追加返回信息。 + - 新增:SSO章节文档新增用户数据同步/迁移方案的建议。 - 新增插件/示例: - 新增:新增插件 sa-token-hutool-timed-cache,用于整合 Hutool 缓存插件 TimedCache。 **[重要]** - 新增:新增 SSM 架构整合 Sa-Token 简单示例。 **[重要]** diff --git a/sa-token-doc/sso/user-data-sync.md b/sa-token-doc/sso/user-data-sync.md new file mode 100644 index 00000000..e5f018f3 --- /dev/null +++ b/sa-token-doc/sso/user-data-sync.md @@ -0,0 +1,234 @@ +# 用户数据同步 / 迁移 + +本篇文章仅提供架构设计的略微参考,真实场景中每个公司的架构设计都是千差万别的,一套设计理论未必能够适应所有公司的项目。 +所以如果你觉着本篇文章的设计理念不能契合你公司的需求,请以你公司的原设计为准。 + +--- + +### 数据同步需求 + +在前面的不同架构 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); + } + + // 进行登录 + StpUtil.login(user.getId(), ctr.remainSessionTimeout); + StpUtil.getSession().set("user", user); + + // 一切工作完毕,重定向回 back 页面 + return SaHolder.getResponse().redirect(back); + }; + +} +``` + +至此完毕。 + + + +