diff --git a/sa-token-doc/_sidebar.md b/sa-token-doc/_sidebar.md index e89bd6b0..a934e21c 100644 --- a/sa-token-doc/_sidebar.md +++ b/sa-token-doc/_sidebar.md @@ -45,11 +45,15 @@ - [配置域名校验](/sso/sso-check-domain) - [定制化登录页面](/sso/sso-custom-login) - [自定义API路由](/sso/sso-custom-api) - - [前后端分离下的整合方案](/sso/sso-h5) - - [NoSdk 模式与非 java 项目](/sso/sso-nosdk) - [平台中心跳转模式](/sso/sso-home-jump) - - [不同 Client 不同秘钥](/sso/sso-diff-key) + - [匿名 client 接入](/sso/anon-client) + - [单点注销](/sso/signout) + - [前后端分离下的整合方案](/sso/sso-h5) + - [消息推送机制](/sso/message-push) + - [用户数据同步 / 迁移](/sso/user-data-sync) + - [NoSdk、ReSdk 模式与非 java 项目](/sso/sso-nosdk) + - [SSO 代码 API 参考](/sso/sso-dev) - [常见问题总结](/sso/sso-questions) - [Sa-Sso-Pro:单点登录商业版](/sso/sso-pro) diff --git a/sa-token-doc/fun/cors-filter.md b/sa-token-doc/fun/cors-filter.md index 4f158623..9b2ac0ed 100644 --- a/sa-token-doc/fun/cors-filter.md +++ b/sa-token-doc/fun/cors-filter.md @@ -2,4 +2,4 @@ - 参考:[https://juejin.cn/post/7247376558367981627](https://juejin.cn/post/7247376558367981627) + 参考:[https://juejin.cn/post/7491603065944129590](https://juejin.cn/post/7491603065944129590) diff --git a/sa-token-doc/sso/anon-client.md b/sa-token-doc/sso/anon-client.md new file mode 100644 index 00000000..13814e34 --- /dev/null +++ b/sa-token-doc/sso/anon-client.md @@ -0,0 +1,108 @@ +# 匿名 Client 接入 + +匿名 Client 就是指在客户端没有配置 `sso-client` 的应用,没有一个明确的 “Client” 标识名称。 + +匿名 Client 在一些关键步骤中不会构建 `client` 参数,如:“重定向至认证中心授权地址”、“校验 ticket”、“单点注销” 等。 + +要想匿名 client 接入,你需要做一些特殊配置。 + + +### 1、在 sso-server 端开启匿名 client 接入 + +开启方式一,通过配置项方式: + + + +``` yaml +# Sa-Token 配置 +sa-token: + # SSO-Server 配置 + sso-server: + # 是否启用匿名 client (开启匿名 client 后,允许客户端接入时不提交 client 参数) + allow-anon-client: true + # 所有允许的授权回调地址 (匿名 client 使用) + allow-url: "*" + # API 接口调用秘钥 (全局默认 + 匿名 client 使用) + secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + +``` properties +# SSO-Server 配置 +# 是否启用匿名 client (开启匿名 client 后,允许客户端接入时不提交 client 参数) +sa-token.sso-server.allow-anon-client=true +# 所有允许的授权回调地址 (匿名 client 使用) +sa-token.sso-server.allow-url=* +# API 接口调用秘钥 (全局默认 + 匿名 client 使用) +sa-token.sso-server.secret-key=kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + + + +开启方式二,通过代码重写方式: + +``` java +/** + * 重写 SaSsoServerTemplate 部分方法,增强功能 + */ +@Component +public class CustomSaSsoServerTemplate extends SaSsoServerTemplate { + + /** + * 获取配置项:是否允许匿名 client 接入 + */ + @Override + public boolean getConfigOfAllowAnonClient() { + return true; + } + + /** + * 获取匿名 client 配置信息 + */ + @Override + public SaSsoClientModel getAnonClient() { + SaSsoClientModel scm = new SaSsoClientModel(); + scm.setAllowUrl("*"); // 允许的授权地址 + scm.setIsSlo(true); // 是否允许单点注销 + scm.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor"); // 客户端密钥 + return scm; + } +} +``` + + +### 2、在 sso-server 端开启匿名 client 接入 + +然后在对应的应用端不要配置 client 字段,例如: + +``` yml +# sa-token配置 +sa-token: + # 配置一个不同的 token-name,以避免在和模式三 demo 一起测试时发生数据覆盖 + token-name: satoken-client-anon + # sso-client 相关配置 + sso-client: + # client 标识 匿名应用就是指不配置 client 标识的应用 + # client: sso-client3 + # sso-server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # 使用 Http 请求校验ticket (模式三) + is-http: true + # 是否在登录时注册单点登录回调接口 (匿名应用想要参与单点注销必须打开这个) + reg-logout-call: true + # API 接口调用秘钥 + secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + + +> [!TIP| label:demo] +> 匿名 Client 接入的 Demo 示例地址:[sa-token-demo-sso3-client-anon](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon) + +这里有个值得注意的配置项:`reg-logout-call: true`,是干嘛的? + +简单来讲,就是匿名应用不包含 client 字段信息,因此 sso-server 端也无法配置此 client 的消息推送地址,所以此 client 无法接受到消息推送,也就无法参与到单点注销的环路中来。 + +因此,新增一个配置项 `reg-logout-call: true`,代表在登录的同时把当前项目的单点注销回调地址 `/sso/logoutCall` 发送到 sso-server 端, +这样 sso-server 端有了备案,也就可以成功通知此应用发起单点注销掉了。 + +如果当前应用不需要单点注销可以不配置此字段。 + diff --git a/sa-token-doc/sso/message-push.md b/sa-token-doc/sso/message-push.md new file mode 100644 index 00000000..70840e6c --- /dev/null +++ b/sa-token-doc/sso/message-push.md @@ -0,0 +1,107 @@ +# 消息推送机制 + +消息推送机制简单来讲就是:sso-client 端按照特点格式构建一个 http 请求,调用 sso-server 端的 `/sso/pushS` 接口,sso-server 接收到消息后做出处理回应 sso-client 端。 + +消息推送是相互的,sso-server 端也可以构建 http 请求,调用 sso-client 端的 `/sso/pushC` 接口。 + +消息推送机制是应用与认证中心相互沟通的桥梁,ticket 校验、单点注销等行为都是依赖消息推送机制来实现的。 + +当然你也可以通过自定义消息处理器的方式,来扩展消息推送能力,这将非常有助于你完成一些应用与认证中心的自定义数据交互。 + +假设我们现在有如下需求:在 sso-client 获取 sso-server 端指定账号 id 的昵称、头像等信息,即:用户资料的拉取。 + + +### 1、认证中心自定义消息处理器 + +首先,我们需要在 sso-server 实现一个消息处理器: + +``` java +@RestController +public class SsoServerController { + + // 配置SSO相关参数 + @Autowired + private void configSso(SaSsoServerTemplate ssoServerTemplate) { + + // 添加消息处理器:userinfo (获取用户资料) (用于为 client 端开放拉取数据的接口) + ssoServerTemplate.messageHolder.addHandle("userinfo", (ssoTemplate, message) -> { + System.out.println("收到消息:" + message); + + // 自定义返回结果(模拟) + return SaResult.ok() + .set("id", message.get("loginId")) + .set("name", "LinXiaoYu") + .set("sex", "女") + .set("age", 18); + }); + + } + +} +``` + + +### 2、应用端调用消息推送接口获取数据 + +首先保证在配置文件里要配置上消息推送的具体地址 + + + +``` yaml +# sa-token配置 +sa-token: + # sso-client 相关配置 + sso-client: + # 应用标识 + client: sso-client3 + # sso-server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # sso-server 端推送消息地址 + # 配置 server-url 后,框架可自动计算对应的 push-url 地址,也可以单独配置 push-url 地址,两者选其一即可 + # push-url: http://sa-sso-server.com:9000/sso/pushS + # API 接口调用秘钥 + secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + +``` properties +# sso-client 相关配置 +# 应用标识 +sa-token.sso-client.client=sso-client3 +# sso-server 端主机地址 +sa-token.sso-client.server-url=http://sa-sso-server.com:9000 +# sso-server 端推送消息地址 +# 配置 server-url 后,框架可自动计算对应的 push-url 地址,也可以单独配置 push-url 地址,两者选其一即可 +sa-token.sso-client.push-url=http://sa-sso-server.com:9000/sso/pushS +# API 接口调用秘钥 +sa-token.sso-client.secret-key=SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + + + +然后在需要拉取资料的地方: + +``` java +// 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 +@RequestMapping("/sso/myInfo") +public Object myInfo() { + // 如果尚未登录 + if( ! StpUtil.isLogin()) { + return "尚未登录,无法获取"; + } + + // 获取本地 loginId + Object loginId = StpUtil.getLoginId(); + + // 构建消息对象 + SaSsoMessage message = new SaSsoMessage(); + message.setType("userinfo"); + message.set("loginId", loginId); + + // 推送至 sso-server,并接收响应数据 + SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); + + // 返回给前端 + return result; +} +``` + diff --git a/sa-token-doc/sso/signout.md b/sa-token-doc/sso/signout.md new file mode 100644 index 00000000..09fa1e38 --- /dev/null +++ b/sa-token-doc/sso/signout.md @@ -0,0 +1,158 @@ +# 单点注销 + +Sa-Token SSO 提供多种注销模式: + +从注销范围上可以分为: + +- 单端注销:会话只在当前应用注销,其它应用和认证中心不受影响。 +- 全端注销:一处注销,全端下线。也即:单点注销。 +- 单浏览器注销:该账号的只在当前浏览器登录的应用注销,其它浏览器/设备不受影响。 + +从注销方式上可以分为: +- ajax 无刷单点注销:调用指定的 RestAPI 接口完成注销。 +- 跳页面注销:跳转到指定接口进行注销,注销完成后原路返回或跳转到指定页面。 + +--- + +### 1、单端注销 + +在后端添加接口: +``` java +// 当前应用独自注销 (不退出其它应用) +@RequestMapping("/sso/logoutByAlone") +public Object logoutByAlone() { + StpUtil.logout(); + return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); +} +``` + +在前端或跳转或 ajax 异步调用此接口即可。 + +如果是跳转可指定 back 参数,代表注销成功后跳转的地址,例如:`http://sso-client.com/sso/logoutByAlone?back=https://sa-token.cc` + + +### 2、全端注销 + +此处先简单看一下 Sa-Token SSO 的单点注销链路过程: + +1. sso-client 的前端向 sso-client 的后端发起单点注销请求。(调用 `http://{sso-client}/sso/logout`) +2. sso-client 的后端向 sso-server 的后端发送单点注销请求。(调用 `http://{sso-server}/sso/pushS?msgType=signout`) +3. sso-server 端遍历 client 列表,逐个推送消息通知 sso-client 端下线。(`http://{sso-client}/sso/pushC?msgType=logoutCall`) +4. sso-server 端注销下线。 +5. sso-server 后端响应 sso-client 后端:注销完成。 +6. sso-client 后端响应 sso-client 前端:注销完成。 +7. 整体完成。 + + + + + +这些逻辑 Sa-Token 内部已经封装完毕,你只需按照文档步骤集成即可。以模式三 demo 为例: + +#### 2.1、更改注销方案 + +单点注销是 Sa-Token SSO 内部已封装的接口,无需手动再添加,只需要在前端调用即可。 + +``` java +// SSO-Client端:首页 +@RequestMapping("/") +public String index() { + String str = "

Sa-Token SSO-Client 应用端 (模式三)

" + + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + + "

" + + "登录 - " + + "单应用注销 - " + + "全端注销 " + + "

"; + return str; +} +``` + +重点在第 9 行。 + +#### 2.2、启动测试 +重启项目,依次登录三个 client: +- [http://sa-sso-client1.com:9003/](http://sa-sso-client1.com:9003/) +- [http://sa-sso-client2.com:9003/](http://sa-sso-client2.com:9003/) +- [http://sa-sso-client3.com:9003/](http://sa-sso-client3.com:9003/) + +![sso-type3-client-index.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type3-client-index.png 's-w-sh') + +在任意一个 client 里,点击 **`[注销]`** 按钮,即可单点注销成功(打开另外两个client,刷新一下页面,登录态丢失)。 + + + +![sso-type3-slo-index.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type3-slo-index.png 's-w-sh') + +PS:这里我们为了方便演示,使用的是超链接跳页面的形式,正式项目中使用 Ajax 调用接口即可做到无刷单点登录退出。 + +例如,我们使用 [Apifox 接口测试工具](https://www.apifox.cn/) 可以做到同样的效果: + +![sso-slo-apifox.png](https://oss.dev33.cn/sa-token/doc/sso/sso-slo-apifox.png 's-w-sh') + + + +### 3、单浏览器注销 + +单浏览器注销的前提是在登录时按照 `deviceId` 设备ID 参数为登录进行分组,这样在发起注销时即可格局设备ID参数做到单浏览器注销功能。 + +#### 3.1、sso-server 端加上设备ID参数登录 + +首先在 sso-server 的登录方法内,加上 deviceId 参数,例如: + +``` java +@RestController +public class SsoServerController { + + // 其它代码,非重点,省略展示... + + // 配置SSO相关参数 + @Autowired + private void configSso(SaSsoServerTemplate ssoServerTemplate) { + // 配置:登录处理函数 + ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> { + // 此处仅做模拟登录,真实环境应该查询数据库进行登录 + if("sa".equals(name) && "123456".equals(pwd)) { + String deviceId = SaHolder.getRequest().getParam("deviceId", SaFoxUtil.getRandomString(32)); + StpUtil.login(10001, new SaLoginParameter().setDeviceId(deviceId)); + return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue()); + } + return SaResult.error("登录失败!"); + }; + + } +} +``` + +如上代码,在登录时获取前端提交的 deviceId 参数,如果前端没有提交则随机生成一个。 + + +#### 3.2、sso-client 端发起注销时指定单设备注销参数 + +然后在 sso-client 发起单点注销时,加上 `singleDeviceIdLogout=true` 参数,代表按照设备 id 进行分组注销,非本设备id的会话不参与注销行为: + +``` java +// SSO-Client端:首页 +@RequestMapping("/") +public String index() { + String str = "

Sa-Token SSO-Client 应用端 (模式三)

" + + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + + "

" + + "登录 - " + + "单应用注销 - " + + "单浏览器注销 - " + + "全端注销 " + + "

"; + return str; +} +``` + +重点在第 9 行。 + + +> [!WARNING| label:测试注意点] +> 在进行测试时,同时将一个浏览器双击打开两次,是不算 “不同浏览器” 的,虽然你打开了两个浏览器窗口,但是这两个浏览器的会话数据是互通的。 +> +> 必须打开两个不同的浏览器来测试,或者按快捷键 `ctrl + shift + N` 打开隐私模式,才可以做到会话相互隔离。 + + diff --git a/sa-token-doc/sso/sso-apidoc.md b/sa-token-doc/sso/sso-apidoc.md index 093a6e4f..1926ca9e 100644 --- a/sa-token-doc/sso/sso-apidoc.md +++ b/sa-token-doc/sso/sso-apidoc.md @@ -20,7 +20,7 @@ http://{host}:{port}/sso/auth | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | -| redirect | 是 | 登录成功后的重定向地址,一般填写 location.href(从哪来回哪去) | +| redirect | 否 | 登录成功后的重定向地址,一般填写 location.href(从哪来回哪去),如不填,则跳转至 home-route | | mode | 否 | 授权模式,取值 [simple, ticket],simple=登录后直接重定向,ticket=带着ticket参数重定向,默认值为ticket | | client | 否 | 客户端标识,可不填,代表是一个匿名应用,若填写了,则校验 ticket 时也必须是这个 client 才可以校验成功 | @@ -28,12 +28,13 @@ http://{host}:{port}/sso/auth - 情况一:当前会话在 SSO 认证中心未登录,会进入登录页开始登录。 - 情况二:当前会话在 SSO 认证中心已登录,会被重定向至 `redirect` 地址,并携带 `ticket` 参数。 -`ticket` 码具有以下特点: +Ticket 码具有以下特点: 1. 每次授权产生的 `ticket` 码都不一样。 2. `ticket` 码用完即废,不能二次使用。 3. 一个 `ticket` 的有效期默认为五分钟,超时自动作废。 4. 每次授权产生新 `ticket` 码,会导致旧 `ticket` 码立即作废,即使旧 `ticket` 码尚未使用。 + ### 2、RestAPI 登录接口 ``` url http://{host}:{port}/sso/doLogin @@ -46,42 +47,63 @@ http://{host}:{port}/sso/doLogin | name | 是 | 用户名 | | pwd | 是 | 密码 | -此接口属于 RestAPI (使用ajax访问),会进入后端配置的 `ssoServer.doLoginHandle` 函数中,此函数的返回值即是此接口的响应值。 +此接口属于 RestAPI (使用ajax访问),会进入后端配置的 `ssoServerTemplate.strategy.doLoginHandle` 函数中,此函数的返回值即是此接口的响应值。 另外需要注意:此接口并非只能携带 name、pwd 参数,因为你可以在方法里通过 `SaHolder.getRequest().getParam("xxx")` 来获取前端提交的其它参数。 -### 3、Ticket 校验接口 -此接口仅配置模式三 `(isHttp=true)` 时打开 +### 3、单点注销接口 +``` url +http://{host}:{port}/sso/signout +``` + +接受参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| back | 否 | 注销成功后的重定向地址,一般填写 location.href(从哪来回哪去),也可以填写 self 字符串,含义同上 | + + +### 4、消息推送接口 ``` url -http://{host}:{port}/sso/checkTicket +http://{host}:{port}/sso/pusS ``` 接收参数: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | -| ticket | 是 | 在步骤 1 中授权重定向时的 ticket 参数 | -| ssoLogoutCall | 否 | 单点注销时的回调通知地址,只在SSO模式三单点注销时需要携带此参数| -| client | 否 | 客户端标识,可不填,代表是一个匿名应用,若填写了,则必须填写的和 `/sso/auth` 登录时填写的一致才可以校验成功 | +| client | 否 | 客户端标识,可不填,代表是一个匿名应用 | | timestamp | 是 | 当前时间戳,13位 | | nonce | 是 | 随机字符串 | -| sign | 是 | 签名,生成算法:`md5( [client={client值}&]nonce={随机字符串}&[ssoLogoutCall={单点注销回调地址}&]ticket={ticket值}×tamp={13位时间戳}&key={secretkey秘钥} )` 注:[]内容代表可选 | +| sign | 是 | 签名,生成算法:`md5( client={client值}&nonce={随机字符串}×tamp={13位时间戳}&key={secretkey秘钥} )` | -返回值场景: -- 校验成功时: +此接口可根据消息类型增加任意参数。新增加的参数要参与 sign 签名。 + +返回值示例: + +- 推送成功时: ``` js { "code": 200, "msg": "ok", - "data": "10001", // 此 ticket 指向的 loginId - "remainSessionTimeout": 7200, // 此账号在 sso-server 端的会话剩余有效期(单位:s) + "data": "10001", // 返回的数据 } ``` -- 校验失败时: +- 推送失败时: + +``` js +{ + "code": 500, // 200表示请求成功,非200标识请求失败 + "msg": "签名无效:xxx", // 失败原因 + "data": null +} +``` + +- 也有可能消息推送成功了,但是处理消息失败,例如校验 ticket 时: ``` js { @@ -92,58 +114,6 @@ http://{host}:{port}/sso/checkTicket ``` -### 4、单点注销接口 -``` url -http://{host}:{port}/sso/signout -``` - -此接口有两种调用方式 - -##### 4.1、方式一:在 Client 的前端页面引导用户直接跳转,并带有 back 参数 -例如: - -``` url -http://{host}:{port}/sso/signout?back=xxx -``` -用户注销成功后将返回 back 地址 - -##### 4.2、方式二:在 Client 的后端通过 http 工具来调用 - -接受参数: - -| 参数 | 是否必填 | 说明 | -| :-------- | :-------- | :-------- | -| loginId | 是 | 要注销的账号 id | -| timestamp | 是 | 当前时间戳,13位 | -| nonce | 是 | 随机字符串 | -| sign | 是 | 签名,生成算法:`md5( loginId={账号id}&nonce={随机字符串}×tamp={13位时间戳}&key={secretkey秘钥} )` | -| client | 否 | 客户端标识,可不填,一般在帮助 “sso-server 端不同client不同秘钥” 的场景下找到对应秘钥时,才填写 | - -例如: -``` url -http://{host}:{port}/sso/signout?loginId={value}×tamp={value}&nonce={value}&sign={value} -``` - -将返回 json 数据结果,形如: - -``` js -{ - "code": 200, // 200表示请求成功,非200标识请求失败 - "msg": "单点注销成功", - "data": null -} -``` - -如果单点注销失败,将返回: - -``` js -{ - "code": 500, // 200表示请求成功,非200标识请求失败 - "msg": "签名无效:xxx", // 失败原因 - "data": null -} -``` -
@@ -197,7 +167,7 @@ http://{host}:{port}/sso/logout ### 3、单点注销回调接口 -此接口仅配置模式三 `(isHttp=true)` 时打开,且为框架回调,开发者无需关心 +此接口仅配置 `(reg-logout-call=true)` 时打开,且为框架回调,开发者无需关心 ``` url http://{host}:{port}/sso/logoutCall @@ -227,6 +197,45 @@ http://{host}:{port}/sso/logoutCall +### 4、消息推送接口 + +``` url +http://{host}:{port}/sso/pusC +``` + +接收参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| timestamp | 是 | 当前时间戳,13位 | +| nonce | 是 | 随机字符串 | +| sign | 是 | 签名,生成算法:`md5( nonce={随机字符串}×tamp={13位时间戳}&key={secretkey秘钥} )` | + +此接口可根据消息类型增加任意参数。新增加的参数要参与 sign 签名。 + +返回值示例: + +- 推送成功时: + +``` js +{ + "code": 200, + "msg": "ok", + "data": "10001", // 返回的数据 +} +``` + +- 推送失败时: + +``` js +{ + "code": 500, // 200表示请求成功,非200标识请求失败 + "msg": "签名无效:xxx", // 失败原因 + "data": null +} +``` + + diff --git a/sa-token-doc/sso/sso-check-domain.md b/sa-token-doc/sso/sso-check-domain.md index 1af4ea02..12992e3c 100644 --- a/sa-token-doc/sso/sso-check-domain.md +++ b/sa-token-doc/sso/sso-check-domain.md @@ -3,19 +3,19 @@ --- ### 1、Ticket劫持攻击 -在前面章节的 SSO-Server 示例中,配置项 `sa-token.sso-server.allow-url=*` 意为配置所有允许的Client端授权地址,不在此配置项中的URL将无法单点登录成功 +在前面章节的 SSO-Server 示例中,配置项 `sa-token.sso-server.clients.sso-client3.allow-url=*` 意为改 client 所有允许的授权地址,不在此配置项中的 URL 将无法单点登录成功。 -为了方便测试,上述代码将其配置为`*`,但是,在生产环境中,此配置项绝对不能配置为 * ,否则会有被 Ticket 劫持的风险 +为了方便测试,上述代码将其配置为`*`,但是,在生产环境中,此配置项绝对不能配置为 * ,否则会有被 Ticket 劫持的风险。 -假设攻击者根据模仿我们的授权地址,巧妙的构造一个URL +假设攻击者根据模仿我们的授权地址,巧妙的构造一个URL: -> [http://sa-sso-server.com:9000/sso/auth?redirect=https://www.baidu.com/](http://sa-sso-server.com:9000/sso/auth?redirect=https://www.baidu.com/) +> [http://sa-sso-server.com:9000/sso/auth?client=sso-client3&redirect=https://www.baidu.com/](http://sa-sso-server.com:9000/sso/auth?client=sso-client3&redirect=https://www.baidu.com/) -当不知情的小红被诱导访问了这个URL时,它将被重定向至百度首页 +当不知情的小红被诱导访问了这个URL时,它将被重定向至百度首页: ![sso-ticket-jc](https://oss.dev33.cn/sa-token/doc/sso/sso-ticket-jc.png 's-w-sh') -可以看到,代表着用户身份的 Ticket 码也显现到了URL之中,借此漏洞,攻击者完全可以构建一个URL将小红的 Ticket 码自动提交到攻击者自己的服务器,伪造小红身份登录网站 +可以看到,代表着用户身份的 Ticket 码也显现到了 URL 之中,借此漏洞,攻击者完全可以构建一个URL将小红的 Ticket 码自动提交到攻击者自己的服务器,伪造小红身份登录网站 ### 2、防范方法 @@ -27,13 +27,15 @@ ``` yaml sa-token: sso-server: - # 配置允许单点登录的 url - allow-url: http://sa-sso-client1.com:9001/sso/login + clients: + sso-client3: + # 配置允许单点登录的 url + allow-url: http://sa-sso-client1.com:9003/sso/login ``` ``` properties # 配置允许单点登录的 url -sa-token.sso-server.allow-url=http://sa-sso-client1.com:9001/sso/login +sa-token.sso-server.clients.so-client3.allow-url=http://sa-sso-client1.com:9003/sso/login ``` diff --git a/sa-token-doc/sso/sso-custom-api.md b/sa-token-doc/sso/sso-custom-api.md index fb734e82..c82fa749 100644 --- a/sa-token-doc/sso/sso-custom-api.md +++ b/sa-token-doc/sso/sso-custom-api.md @@ -33,13 +33,11 @@ public class SsoServerController { ``` java // 配置SSO相关参数 @Autowired -private void configSso(SaSsoServerConfig ssoServer) { +private void configSso(SaSsoServerTemplate ssoServerTemplate) { // 自定义API地址 SaSsoServerProcessor.instance.ssoServerTemplate.apiName.ssoAuth = "/sso/auth2"; // ... - // SSO 相关配置 - ssoServer.xxx ... ; } ``` @@ -70,10 +68,10 @@ public class SsoServerController { return SaSsoServerProcessor.instance.ssoDoLogin(); } - // SSO-Server:校验ticket 获取账号id - @RequestMapping("/sso/checkTicket") - public Object ssoCheckTicket() { - return SaSsoServerProcessor.instance.ssoCheckTicket(); + // SSO-Server:接收推送消息地址 + @RequestMapping("/sso/pushS") + public Object ssoPushS() { + return SaSsoServerProcessor.instance.ssoPushS(); } // SSO-Server:单点注销 @@ -89,3 +87,41 @@ public class SsoServerController { 拆分式路由 与 聚合式路由 在功能上完全等价,且提供了更为细致的路由管控。 + +### SSO-Client 端拆分路由入口示例 + +``` java +/** + * Sa-Token-SSO Client端 Controller + */ +@RestController +public class SsoClientController { + + // SSO-Client:登录地址 + @RequestMapping("/sso/login") + public Object ssoLogin() { + return SaSsoClientProcessor.instance.ssoLogin(); + } + + // SSO-Client:单点注销地址 + @RequestMapping("/sso/logout") + public Object ssoLogout() { + return SaSsoClientProcessor.instance.ssoLogout(); + } + + // SSO-Client:单点注销回调 + @RequestMapping("/sso/logoutCall") + public Object ssoLogoutCall() { + return SaSsoClientProcessor.instance.ssoLogoutCall(); + } + + // SSO-Client:接收消息推送地址 + @RequestMapping("/sso/ssoPushC") + public Object ssoPushC() { + return SaSsoClientProcessor.instance.ssoPushC(); + } + + // ... 其它方法 + +} +``` \ No newline at end of file diff --git a/sa-token-doc/sso/sso-custom-login.md b/sa-token-doc/sso/sso-custom-login.md index c01b95a9..dc5dcc0d 100644 --- a/sa-token-doc/sso/sso-custom-login.md +++ b/sa-token-doc/sso/sso-custom-login.md @@ -81,7 +81,7 @@ if(res.code == 401) { ``` java // 配置:未登录时返回的View -sso.notLoginView = () -> { +ssoServerTemplate.strategy.notLoginView = () -> { return new ModelAndView("xxx.html"); } ``` @@ -119,7 +119,7 @@ public SaResult ss(String name, String pwd) { 如果你的不同应用覆盖的用户群体差异极大,此时你可能想针对不同的应用跳转到不同的登录页,让每个应用的用户在登录时能够看到当前应用的专属信息,怎么做呢? -首先,你需要在每个 sso-client 端配置上不同的 client 标识: +首先,保证每个 sso-client 端都配置了不同的 client 标识: @@ -143,10 +143,10 @@ sa-token.sso-client.client=sso-client-shop ``` java // 配置SSO相关参数 @Autowired -private void configSso(SaSsoServerConfig ssoServer) { +private void configSso(SaSsoServerTemplate ssoServerTemplate) { // 配置:未登录时返回的View - ssoServer.notLoginView = () -> { + ssoServerTemplate.strategy.notLoginView = () -> { String client = SaHolder.getRequest().getParam("client"); if("sso-client-shop".equals(client)) { diff --git a/sa-token-doc/sso/sso-dev.md b/sa-token-doc/sso/sso-dev.md new file mode 100644 index 00000000..d3368bf7 --- /dev/null +++ b/sa-token-doc/sso/sso-dev.md @@ -0,0 +1,143 @@ +# Sa-Token SSO Server端 二次开发用到的所有函数说明 + +本篇展示一下 SSO 模块常用的工具类、方法 + +## Sso-Server 工具类 + +### Ticket 操作 +``` java +// 增删改 + +// 删除 Ticket +SaSsoServerUtil.deleteTicket(String ticket); + +// 根据参数创建一个 ticket 码,并保存 +SaSsoServerUtil.createTicketAndSave(String client, Object loginId, String tokenValue); + +// 查 + +// 查询 ticket ,如果 ticket 无效则返回 null +SaSsoServerUtil.getTicket(String ticket); + +// 查询 ticket 指向的 loginId,如果 ticket 码无效则返回 null +SaSsoServerUtil.getLoginId(String ticket); + +// 查询 ticket 指向的 loginId,并转换为指定类型 +SaSsoServerUtil.getLoginId(String ticket, Class cs); + +// 校验 + +// 校验 Ticket,无效 ticket 会抛出异常 +SaSsoServerUtil.checkTicket(String ticket); + +// 校验 Ticket 码,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 +SaSsoServerUtil.checkTicketParamAndDelete(String ticket); + +// 校验 Ticket,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 +SaSsoServerUtil.checkTicketParamAndDelete(String ticket, String client); + +// ticket 索引 + +// 查询 指定 client、loginId 其所属的 ticket 值 +SaSsoServerUtil.getTicketValue(String client, Object loginId); +``` + + + +### Client 信息获取 +``` java +// 获取所有 Client +SaSsoServerUtil.getClients(); + +// 获取应用信息,无效 client 返回 null +SaSsoServerUtil.getClient(String client); + +// 获取应用信息,无效 client 则抛出异常 +SaSsoServerUtil.getClientNotNull(String client); + +// 获取匿名 client 信息 +SaSsoServerUtil.getAnonClient(); + +// 获取所有需要接收消息推送的 Client +SaSsoServerUtil.getNeedPushClients(); +``` + + +### 重定向 URL 构建与校验 +``` java +// 构建 URL:sso-server 端向 sso-client 下放 ticket 的地址 +SaSsoServerUtil.buildRedirectUrl(String client, String redirect, Object loginId, String tokenValue); + +// 校验重定向 url 合法性 +SaSsoServerUtil.checkRedirectUrl(String client, String url); +``` + + +### 单点注销 +``` java +// 指定账号单点注销 +SaSsoServerUtil.ssoLogout(Object loginId); + +// 指定账号单点注销 +SaSsoServerUtil.ssoLogout(Object loginId, SaLogoutParameter logoutParameter); +``` + + + + + +### 消息推送 +``` java +// 向指定 Client 推送消息 +SaSsoServerUtil.pushMessage(SaSsoClientModel clientModel, SaSsoMessage message); + +// 向指定 client 推送消息,并将返回值转为 SaResult +SaSsoServerUtil.pushMessageAsSaResult(SaSsoClientModel clientModel, SaSsoMessage message); + +// 向指定 Client 推送消息 +SaSsoServerUtil.pushMessage(String client, SaSsoMessage message); + +// 向指定 client 推送消息,并将返回值转为 SaResult +SaSsoServerUtil.pushMessageAsSaResult(String client, SaSsoMessage message); + +// 向所有 Client 推送消息 +SaSsoServerUtil.pushToAllClient(SaSsoMessage message); + +// 向所有 Client 推送消息,并忽略掉某个 client +SaSsoServerUtil.pushToAllClient(SaSsoMessage message, String ignoreClient); +``` + + +详情请参考源码:[码云:SaSsoServerUtil.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerUtil.java) + + + +## Sso-Client 工具类 + + +### 构建交互地址 +``` java +// 构建URL:Server端 单点登录授权地址 +SaSsoClientUtil.buildServerAuthUrl(String clientLoginUrl, String back); +``` + + +### 消息推送 + +``` java +// 向 sso-server 推送消息 +SaSsoClientUtil.pushMessage(SaSsoMessage message); + +// 向 sso-server 推送消息,并将返回值转为 SaResult +SaSsoClientUtil.pushMessageAsSaResult(SaSsoMessage message); + +// 构建消息:校验 ticket +SaSsoClientUtil.buildCheckTicketMessage(String ticket, String ssoLogoutCallUrl); + +// 构建消息:单点注销 +SaSsoClientUtil.buildSignoutMessage(Object loginId, SaLogoutParameter logoutParameter); +``` + +详情请参考源码:[码云:SaSsoClientUtil.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoClientUtil.java) + + diff --git a/sa-token-doc/sso/sso-h5.md b/sa-token-doc/sso/sso-h5.md index 1aa5d6ba..f0b02187 100644 --- a/sa-token-doc/sso/sso-h5.md +++ b/sa-token-doc/sso/sso-h5.md @@ -2,135 +2,321 @@ --- -如果我们已有的系统是前后端分离模式,我们显然不能为了接入SSO而改造系统的基础架构,官方仓库的示例采用的是前后端一体方案,要将其改造为前后端分离架构模式非常简单 +## SSO-Client 前后端分离 -以`sa-token-demo-sso2-client`为例: +要在前后端分离的环境中接入 SSO,思路不难,主要的工作是吧后端 `/sso/login` 接口的路由中转工作拿到前端来,以`sa-token-demo-sso3-client`为例: + +### 1、在 sso-client 后端新建`H5Controller`,开放接口: -### 1、新建`H5Controller`开放接口 ``` java /** - * 前后端分离架构下集成SSO所需的接口 + * 前后台分离架构下集成 SSO 所需的代码 (SSO-Client端) */ @RestController public class H5Controller { - // 当前是否登录 + // 判断当前是否登录 @RequestMapping("/sso/isLogin") public Object isLogin() { - return SaResult.data(StpUtil.isLogin()); + return SaResult.data(StpUtil.isLogin()).set("loginId", StpUtil.getLoginIdDefaultNull()); } // 返回SSO认证中心登录地址 @RequestMapping("/sso/getSsoAuthUrl") public SaResult getSsoAuthUrl(String clientLoginUrl) { - String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(clientLoginUrl, ""); + String serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, ""); return SaResult.data(serverAuthUrl); } - // 根据ticket进行登录 + // 根据 ticket 进行登录 @RequestMapping("/sso/doLoginByTicket") public SaResult doLoginByTicket(String ticket) { - SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket, "/sso/doLoginByTicket"); - StpUtil.login(ctr.loginId, ctr.remainSessionTimeout); + SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket); + StpUtil.login(ctr.loginId, new SaLoginParameter() + .setTimeout(ctr.remainTokenTimeout) + .setDeviceId(ctr.deviceId) + ); return SaResult.data(StpUtil.getTokenValue()); } - // 全局异常拦截 - @ExceptionHandler - public SaResult handlerException(Exception e) { - e.printStackTrace(); - return SaResult.error(e.getMessage()); - } - } ``` -### 2、增加跨域过滤器`CorsFilter.java` -源码详见:[CorsFilter.java](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/h5/CorsFilter.java), -将其复制到项目中即可 + +### 2、增加跨域处理策略 + +``` java +/** + * [Sa-Token 权限认证] 配置类 + */ +@Configuration +public class SaTokenConfigure { + + /** + * CORS 跨域处理策略 + */ + @Bean + public SaCorsHandleFunction corsHandle() { + return (req, res, sto) -> { + res. + // 允许指定域访问跨域资源 + setHeader("Access-Control-Allow-Origin", "*") + // 允许所有请求方式 + .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") + // 有效时间 + .setHeader("Access-Control-Max-Age", "3600") + // 允许的header参数 + .setHeader("Access-Control-Allow-Headers", "*"); + + // 如果是预检请求,则立即返回到前端 + SaRouter.match(SaHttpMethod.OPTIONS) + .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) + .back(); + }; + } + +} +``` + +详细参考:[解决跨域问题](/fun/cors-filter) + ### 3、新建前端项目 任意文件夹新建前端项目:`sa-token-demo-sso-client-h5`,在根目录添加测试文件:`index.html` -``` js +``` html - Sa-Token-SSO-Client端-测试页(前后端分离版) + Sa-Token-SSO-Client端-测试页(前后端分离版-原生h5) -

Sa-Token SSO-Client 应用端(前后端分离版)

+

Sa-Token SSO-Client 应用端(前后端分离版-原生h5)

当前是否登录:

- 登录 - 注销 + 登录 - + 单应用注销 - + 单浏览器注销 - + 全端注销 - + 账号资料

- + ``` -### 4、添加登录处理文件`sso-login.html` -源码详见:[sso-login.html](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/sso-login.html), -将其复制到项目中即可,与`index.html`一样放在根目录下 +### 4、添加单点登录登录中转页 + +在根目录创建文件:`sso-login.html` + +``` html + + + + + Sa-Token-SSO-Client端-登录中转页页 + + + +
+ 加载中 ... +
+ + + + +``` -### 5、测试运行 -先启动Server服务端与Client服务端,再随便找个能预览html的工具打开前端项目(比如[HBuilderX](https://www.dcloud.io/hbuilderx.html)),测试流程与一体版一致 +### 5、添加公共 js文件 +新建 `sso-common.js`: + +``` js +// 服务器接口主机地址 +// var baseUrl = "http://sa-sso-client1.com:9002"; // 模式二后端 +var baseUrl = "http://sa-sso-client1.com:9003"; // 模式三后端 + +// 封装一下Ajax +function ajax(path, data, successFn, errorFn) { + console.log('发起请求:', baseUrl + path, JSON.stringify(data)); + fetch(baseUrl + path, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + 'satoken': localStorage.getItem('satoken') + }, + body: serializeToQueryString(data), + }) + .then(response => response.json()) + .then(res => { + console.log('返回数据:', res); + if(res.code === 500) { + return alert(res.msg); + } + successFn(res); + }) + .catch(error => { + console.error('请求失败:', error); + return alert("异常:" + JSON.stringify(error)); + }); +} + +// ------------ 工具方法 --------------- + +// 从url中查询到指定名称的参数值 +function getParam(name, defaultValue) { + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split("="); + if (pair[0] == name) { + return pair[1]; + } + } + return (defaultValue == undefined ? null : defaultValue); +} + +// 将 json 对象序列化为kv字符串,形如:name=Joh&age=30&active=true +function serializeToQueryString(obj) { + return Object.entries(obj) + .filter(([_, value]) => value != null) // 过滤 null 和 undefined + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join('&'); +} + +// 向指定标签里 set 内容 +function setHtml(select, html) { + const dom = document.querySelector('.is-login'); + if(dom) { + dom.innerHTML = html; + } +} +``` -### 6、SSO-Server 端的前后端分离 -疑问:上述代码都是针对 Client 端进行拆分,如果我想在 SSO-Server 端也进行前后端分离改造,应该怎么做? +### 6、测试运行 +先启动 Server 服务端与 Client 服务端,再随便找个能预览html的工具打开前端项目(比如[HBuilderX](https://www.dcloud.io/hbuilderx.html)),测试流程与一体版一致,暂不赘述。 -> 答:解决思路都是大同小异的,与Client一样,我们需要把原本在 “后端处理的授权重定向逻辑” 拿到前端来实现。 -由于集成代码与 Client 端类似,这里暂不贴详细代码,我们可以下载官方仓库,里面有搭建好的demo -使用前端ide导入项目 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5`,浏览器访问 `sso-auth.html` 页面: + +> [!TIP| label:另附其它技术栈的前后端分离 demo 示例:] +> - sso-client 前后端分离 - 原生h5:[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5) +> - sso-client 前后端分离 - vue2:[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2) +> - sso-client 前后端分离 - vue3:[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3) + + +## SSO-Server 前后端分离 + +解决思路与 SSO-Client 一样,我们需要把原本在 “后端处理的授权重定向逻辑” 拿到前端来实现。 + +由于集成代码与 Client 端类似,这里暂不贴详细代码,我们可以下载官方仓库,里面有搭建好的demo。 + +使用前端 ide 导入项目 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5`,浏览器访问 `sso-auth.html` 页面: ![sso-type2-server-h5-auth.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type2-server-h5-auth.png 's-w-sh') -复制上述地址,将其配置到 Client 端的 yml 配置文件中,例如: +复制上述地址,将其配置到 Client 端的配置项 `sa-token.sso-client.auth-url` ,例如: ``` yaml sa-token: sso-client: - # SSO-Server端 统一认证地址 + # sso-server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数(上面的不要注释,auth-url 配置项和 server-url 要同时存在) auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html ``` ``` properties # SSO-Server端 统一认证地址 +sa-token.sso-client.server-url=http://sa-sso-server.com:9000 +# 在 sso-server 端前后端分离时需要单独配置 auth-url 参数(上面的不要注释,auth-url 配置项和 server-url 要同时存在) sa-token.sso-client.auth-url=http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html ``` -然后我们启动项目 `sa-token-demo-sso-server` 与 `sa-token-demo-sso2-client`,按照之前的测试步骤访问: -[http://sa-sso-client1.com:9001/](http://sa-sso-client1.com:9001/),即可以前后端分离模式完成 SSO-Server 端的授权登录。 +然后我们启动项目 sso-server 与 sso-client ,按照之前的测试步骤访问: +[http://sa-sso-client1.com:9003/](http://sa-sso-client1.com:9003/),即可以前后端分离模式完成 SSO-Server 端的授权登录。 diff --git a/sa-token-doc/sso/sso-home-jump.md b/sa-token-doc/sso/sso-home-jump.md index 87b24cc3..a7f1b7f4 100644 --- a/sa-token-doc/sso/sso-home-jump.md +++ b/sa-token-doc/sso/sso-home-jump.md @@ -15,22 +15,43 @@ 假设子系统的地址是: ``` url -http://sa-sso-client1.com:9001/ +http://sa-sso-client1.com:9003/ ``` 那么我们改造后的地址就是: ``` url -/sso/auth?redirect=http://sa-sso-client1.com:9001/sso/login?back=http://sa-sso-client1.com:9001/ +/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/ ``` -格式形如:`/sso/auth?redirect=${子系统首页}/sso/login?back=${子系统首页}` +格式形如:`/sso/auth?client={client标识}&redirect=${子系统首页}/sso/login?back=${子系统首页}` --- ### 完整代码示例: -1、在 sso-server 中添加 `HomeController`,作为平台中心首页: +1、在 sso-server 中配置 `home-route` 字段: + + + +``` yaml +# Sa-Token 配置 +sa-token: + # SSO-Server 配置 + sso-server: + # 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 + home-route: /home +``` + +``` properties +# 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 +sa-token.sso-server.home-route: /home +``` + + + + +2、在 sso-server 中添加 `HomeController`,作为平台中心首页: ``` java /** @@ -39,51 +60,33 @@ http://sa-sso-client1.com:9001/ @RestController public class HomeController { // 平台化首页 - @RequestMapping("/home") - public Object index() { - // 如果未登录,则先去登录 - if(!StpUtil.isLogin()) { - return SaHolder.getResponse().redirect("/sso/auth"); - } - - // 拼接各个子系统的地址,格式形如:/sso/auth?redirect=${子系统首页}/sso/login?back=${子系统首页} - String link1 = "/sso/auth?redirect=http://sa-sso-client1.com:9001/sso/login?back=http://sa-sso-client1.com:9001/"; - String link2 = "/sso/auth?redirect=http://sa-sso-client2.com:9001/sso/login?back=http://sa-sso-client2.com:9001/"; - String link3 = "/sso/auth?redirect=http://sa-sso-client3.com:9001/sso/login?back=http://sa-sso-client3.com:9001/"; + @RequestMapping({"/", "/home"}) + public Object index() { + // 如果未登录,则先去登录 + if(!StpUtil.isLogin()) { + return SaHolder.getResponse().redirect("/sso/auth"); + } - // 组织网页结构返回到前端 - String title = "

SSO 平台首页

"; - String client1 = "

进入Client1系统

"; - String client2 = "

进入Client2系统

"; - String client3 = "

进入Client3系统

"; - - return title + client1 + client2 + client3; - } + // 拼接各个子系统的地址,格式形如:/sso/auth?client=xxx&redirect=${子系统首页}/sso/login?back=${子系统首页} + String link1 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/"; + String link2 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client2.com:9003/sso/login?back=http://sa-sso-client2.com:9003/"; + String link3 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client3.com:9003/sso/login?back=http://sa-sso-client3.com:9003/"; + + // 组织网页结构返回到前端 + String title = "

SSO 平台首页 (平台中心模式)

"; + String client1 = "

进入Client1系统

"; + String client2 = "

进入Client2系统

"; + String client3 = "

进入Client3系统

"; + + return title + client1 + client2 + client3; + } } ``` -2、修改一下SSO路由处理的代码,使登录后不再重定向到client端,而是跳转到平台中心首页。 - -在 `SsoServerController` 的 `ssoRequest` 方法中添加跳转 `/home` 的代码: - -``` java -// SSO-Server端:处理所有SSO相关请求 -@RequestMapping("/sso/*") -public Object ssoRequest() { - // 如果登录时没有提供redirect参数,则进入平台中心首页 /home,而不是重定向到 client 端 - SaRequest req = SaHolder.getRequest(); - if(req.isPath("/sso/auth") && req.hasParam("redirect") == false && StpUtil.isLogin()) { - return SaHolder.getResponse().redirect("/home"); - } - return SaSsoServerProcessor.instance.dister(); -} -``` - -新加代码在 4-8 行。 ### 测试访问 -启动项目,访问:[http://sa-sso-server.com:9000/home](http://sa-sso-server.com:9000/home) +启动项目,访问:[http://sa-sso-server.com:9000](http://sa-sso-server.com:9000) 首次访问,因为我们没有登录,所以会被重定向到 `/sso/auth` 登录页,我们登录上之后,便会跳转到平台中心首页: diff --git a/sa-token-doc/sso/sso-nosdk.md b/sa-token-doc/sso/sso-nosdk.md index db2d05d8..4d738d1f 100644 --- a/sa-token-doc/sso/sso-nosdk.md +++ b/sa-token-doc/sso/sso-nosdk.md @@ -2,21 +2,37 @@ --- +经常有小伙伴提问:客户端不使用 Sa-Token,能否接入 SSO 认证中心?当然是可以的。 + +SSO-Server 所有接口都是通过 http 协议开放的,这意味着原则上只要一个语言支持 http 请求调用就可以对接 SSO-Server,参考: [SSO 认证中心开放接口](/sso/sso-apidoc) + ### NoSdk 模式 -如果我们的 SSO 应用端不想或不能集成 Sa-Token,则可以使用 NoSdk 模式来对接。 +NoSdk 模式(不使用SDK):通过 http 工具类调用接口的方式来对接 SSO-Server。 -NoSdk 模式:通过 http 工具类调用接口的方式来对接 SSO-Server。 +参考 demo:[sa-token-demo-sso3-client-nosdk](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk) -其实原理很简单,不能集成 Sa-Token 了,那我们就手动写代码模拟出 Sa-Token 在 SSO 流程所做的工作即可。 +该 demo 假设应用端没有使用任何“权限认证框架”,使用最基础的 ServletAPI 进行会话管理,模拟了 `/sso/login`、 `/sso/logout`、 `/sso/logoutCall` 三个接口的处理逻辑。 -由于所需代码较多,无法在文档处直接展示,demo 地址可参考: -[sa-token-demo-sso3-client-nosdk](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk) +> [!WARNING| label:NoSdk 示例不再主维护] +> 基于以下原因: +> - 1、NoSdk demo 相当于通过 http 工具类再次重写了一遍 Sa-Token SSO 模块代码,繁琐且冗余。 +> - 2、重写的代码无法拥有 Sa-Token SSO 模块全部能力,仅能完成基本对接,算是一个简化版 SDK。 +> +> 自 v1.43.0 版本起,不再主维护 NoSdk 模式,仓库示例仅做留档参考,大家可以转为 ReSdk 模式。 -该 demo 假设应用端没有使用任何“权限认证框架”,使用最基础的 ServletAPI 进行会话管理,模拟了 `/sso/login`、`/sso/logout`、`/sso/logoutCall` 三个接口的处理逻辑。 -建议各位同学在阅读源码时结合 [SSO 认证中心开放接口](/sso/sso-apidoc) 观看。 +### ReSdk 模式 +ReSdk 模式(重写SDK部分方法):通过重写框架关键步骤点,来对接 SSO-Server。 + +参考 demo:[sa-token-demo-sso3-client-resdk](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk) + +> [!INFO| label:ReSdk 模式优点] +> - 1、依然支持客户端使用任意技术栈。 +> - 2、仅重写少量部分关键代码,即可完成对接。几乎可以得到 Sa-Token SSO 模块全量能力。 + +建议新项目首选 ReSdk 模式作为参考。 diff --git a/sa-token-doc/sso/sso-questions.md b/sa-token-doc/sso/sso-questions.md index 93649329..c4af93b8 100644 --- a/sa-token-doc/sso/sso-questions.md +++ b/sa-token-doc/sso/sso-questions.md @@ -87,7 +87,7 @@ public class SaSsoServerApplication { ### 问:模式三配置一堆 xxx-url ,有办法简化一下吗? 可以使用 `sa-token.sso-client.server-url` 来简化: -配置含义:配置 Server 端主机总地址,拼接在 authUrl、checkTicketUrl、getDataUrl、sloUrl 属性前面,用以简化各种 url 配置。 +配置含义:配置 Server 端主机总地址,拼接在 authUrl、getDataUrl、sloUrl 属性前面,用以简化各种 url 配置。 在开发 SSO 模块时,我们需要在 sso-client 配置认证中心的各种地址,特别是在模式三下,一般代码会变成这样: @@ -96,8 +96,6 @@ sa-token: sso-client: # SSO-Server端 统一认证地址 auth-url: http://sa-sso-server.com:9000/sso/auth - # SSO-Server端 ticket校验地址 - check-ticket-url: http://sa-sso-server.com:9000/sso/checkTicket # 单点注销地址 slo-url: http://sa-sso-server.com:9000/sso/signout # SSO-Server端 查询数据地址 @@ -214,21 +212,47 @@ public class SaTokenConfigure implements WebMvcConfigurer { -### 问:sa-token.sso-server.allow-url 配置项可以做成从数据库读取的吗? -可以,自定义 `SaSsoServerTemplate` 实现类,重写 `getAllowUrl` 方法即可: +### 问:Client 信息可以做成从数据库读取的吗? +可以,自定义 `SaSsoServerTemplate` 实现类,重写 `getClient` 与 `getClient` 方法即可: ``` java /** - * 重写 SaSsoServerTemplate 部分方法,增强功能 + * 重写 SaSsoServerTemplate 部分方法,增强功能 */ @Component public class CustomSaSsoServerTemplate extends SaSsoServerTemplate { - - // 重写 [获取授权回调地址] 方法,改为从数据库中读取 - @Override - public String getAllowUrl() { - String allowUrl = ""; // 改为从数据库读取 - return allowUrl; - } + + // 获取指定 client 的配置信息 + @Override + public SaSsoClientModel getClient(String client) { + if("sso-client1".equals(client)) { + SaSsoClientModel scm = new SaSsoClientModel(); + scm.setAllowUrl("sso-client1"); + scm.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor"); + return scm; + } + + // ... + + return null; + } + + // 返回所有 client 信息 + @Override + public List getClients() { + // 模拟示例代码,真实项目可改为从数据查询 + + SaSsoClientModel scm1 = new SaSsoClientModel(); + scm1.setAllowUrl("sso-client1"); + scm1.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor"); + + SaSsoClientModel scm2 = new SaSsoClientModel(); + scm2.setAllowUrl("sso-client2"); + scm2.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor"); + + // ... + + return Arrays.asList(scm1, scm2); + } } ``` @@ -258,24 +282,24 @@ public class CustomSaSsoServerTemplate extends SaSsoServerTemplate { public class SsoController { // 处理 SSO-Server 端所有请求 - @RequestMapping({"/sso/auth", "/sso/doLogin", "/sso/checkTicket", "/sso/signout"}) + @RequestMapping({"/sso/auth", "/sso/doLogin", "/sso/signout", "/sso/pushS"}) public Object ssoServerRequest() { return SaSsoServerProcessor.instance.dister(); } // 处理 SSO-Client 端所有请求 - @RequestMapping({"/sso/login", "/sso/logout", "/sso/logoutCall"}) + @RequestMapping({"/sso/login", "/sso/logout", "/sso/logoutCall", "/sso/pushC"}) public Object ssoClientRequest() { return SaSsoClientProcessor.instance.dister(); } // 配置SSO相关参数 @Autowired - private void configSsoServer(SaSsoServerConfig ssoServer) { + private void configSsoServer(SaSsoServerTemplate ssoServerTemplate) { // SSO Server 配置代码,参考文档前几章 ... } @Autowired - private void configSsoClient(SaSsoClientConfig ssoClient) { + private void configSsoClient(SaSsoClientTemplate ssoClientTemplate) { // SSO Client 配置代码,参考文档前几章 ... } @@ -295,7 +319,7 @@ public class SsoController { ``` java /** - * Sa-Token-SSO 第二套 SSO-Server端 Controller + * Sa-Token-SSO 第二套 SSO-Server端 Controller */ @RestController public class SsoUserServerController { @@ -305,21 +329,23 @@ public class SsoUserServerController { */ public static SaSsoServerProcessor ssoUserServerProcessor = new SaSsoServerProcessor(); static { - // 自定义一个 SaSsoTemplate 对象 + + // 自定义一个 getServerConfig + SaSsoServerConfig serverConfig = new SaSsoServerConfig(); + serverConfig.setSecretKey("xxx"); + // 更多配置 ... + + // 自定义一个 SaSsoServerTemplate 对象 SaSsoServerTemplate ssoUserTemplate = new SaSsoServerTemplate() { - // 使用的会话对象 是自定义的 StpUserUtil @Override - public StpLogic getStpLogic() { - return StpUserUtil.stpLogic; - } - // 使用自定义的签名秘钥 - SaSignConfig signConfig = new SaSignConfig().setSecretKey("xxxx-新的秘钥-xxxx"); - SaSignTemplate userSignTemplate = new SaSignTemplate().setSignConfig(signConfig); - @Override - public SaSignTemplate getSignTemplate(String client) { - return userSignTemplate; + public SaSsoServerConfig getServerConfig() { + return serverConfig; } }; + + // 使用自定义的 StpLogic 会话对象 + ssoUserTemplate.setStpLogic(StpUserUtil.stpLogic); + // 让这个SSO请求处理器,使用的路由前缀是 /sso-user,而不是原先的 /sso ssoUserTemplate.apiName.replacePrefix("/sso-user"); @@ -329,10 +355,9 @@ public class SsoUserServerController { /* * 第二套 sso-server 服务:处理所有SSO相关请求 - * http://{host}:{port}/sso-user/auth -- 单点登录授权地址,接受参数:redirect=授权重定向地址 - * http://{host}:{port}/sso-user/doLogin -- 账号密码登录接口,接受参数:name、pwd - * http://{host}:{port}/sso-user/checkTicket -- Ticket校验接口(isHttp=true时打开),接受参数:ticket=ticket码、ssoLogoutCall=单点注销回调地址 [可选] - * http://{host}:{port}/sso-user/signout -- 单点注销地址(isSlo=true时打开),接受参数:loginId=账号id、secretkey=接口调用秘钥 + * http://{host}:{port}/sso-user/auth -- 单点登录授权地址 + * http://{host}:{port}/sso-user/doLogin -- 账号密码登录接口 + * http://{host}:{port}/sso-user/signout -- 单点注销地址(isSlo=true时打开) */ @RequestMapping("/sso-user/*") public Object ssoUserRequest() { diff --git a/sa-token-doc/sso/sso-server.md b/sa-token-doc/sso/sso-server.md index 381942c9..99bdd3c6 100644 --- a/sa-token-doc/sso/sso-server.md +++ b/sa-token-doc/sso/sso-server.md @@ -2,13 +2,13 @@ 在开始SSO三种模式的对接之前,我们必须先搭建一个 SSO-Server 认证中心 -> [!TIP| label:demo | style:callout] +> [!TIP| label:demo] > 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/`,如遇到难点可结合源码进行测试学习,demo里有制作好的登录页面 --- ### 1、添加依赖 -创建 SpringBoot 项目 `sa-token-demo-sso-server`,引入依赖: +创建 SpringBoot 项目 `sa-token-demo-sso-server`,在引入 SpringBoot 依赖的基础上,继续引入: @@ -27,10 +27,10 @@ ${sa.top.version} - + cn.dev33 - sa-token-redis-jackson + sa-token-redis-template ${sa.top.version} @@ -44,11 +44,11 @@ spring-boot-starter-thymeleaf - + - com.dtflys.forest - forest-spring-boot-starter - 1.5.26 + cn.dev33 + sa-token-forest + ${sa.top.version} ``` @@ -59,15 +59,15 @@ implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' // Sa-Token 插件:整合SSO implementation 'cn.dev33:sa-token-sso:${sa.top.version}' -// Sa-Token 整合 Redis (使用 jackson 序列化方式) -implementation 'cn.dev33:sa-token-redis-jackson:${sa.top.version}' +// Sa-Token 整合 RedisTemplate +implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' implementation 'org.apache.commons:commons-pool2' // 视图引擎(在前后端不分离模式下提供视图支持) implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' -// Http请求工具(在模式三的单点注销功能下用到,如不需要可以注释掉) -implementation 'com.dtflys.forest:forest-spring-boot-starter:1.5.26' +// Sa-Token 插件:整合 Forest 请求工具 (模式三需要通过 http 请求推送消息) +implementation 'cn.dev33:sa-token-forest:1.5.26' ``` @@ -75,11 +75,11 @@ implementation 'com.dtflys.forest:forest-spring-boot-starter:1.5.26' > [!NOTE| label:引包简化] > 除了 `sa-token-spring-boot-starter` 和 `sa-token-sso` 以外,其它包都是可选的: > -> - 在 SSO 模式三时 Redis 相关包是可选的 -> - 在前后端分离模式下可以删除 thymeleaf 相关包 -> - 在不需要 SSO 模式三单点注销的情况下可以删除 http 工具包 +> - 在 SSO 模式三时 Redis 相关包是可选的。 +> - 在前后端分离模式下可以删除 thymeleaf 相关包。 +> - 在不需要 SSO 模式三单点注销的情况下可以删除 http 工具包。 > -> 建议先完整测试三种模式之后再对pom依赖进行酌情删减。 +> 建议先完整测试三种模式之后再对 pom 依赖进行酌情删减。 ### 2、开放认证接口 @@ -93,56 +93,52 @@ implementation 'com.dtflys.forest:forest-spring-boot-starter:1.5.26' public class SsoServerController { /** - * SSO-Server端:处理所有SSO相关请求 (下面的章节我们会详细列出开放的接口) + * SSO-Server端:处理所有SSO相关请求 + * http://{host}:{port}/sso/auth -- 单点登录授权地址 + * http://{host}:{port}/sso/doLogin -- 账号密码登录接口,接受参数:name、pwd + * http://{host}:{port}/sso/signout -- 单点注销地址(isSlo=true时打开) */ @RequestMapping("/sso/*") public Object ssoRequest() { return SaSsoServerProcessor.instance.dister(); } - + /** * 配置SSO相关参数 */ @Autowired - private void configSso(SaSsoServerConfig ssoServer) { + private void configSso(SaSsoServerTemplate ssoServerTemplate) { // 配置:未登录时返回的View - ssoServer.notLoginView = () -> { - String msg = "当前会话在SSO-Server端尚未登录,请先访问" - + " doLogin登录 " - + "进行登录之后,刷新页面开始授权"; - return msg; + ssoServerTemplate.strategy.notLoginView = () -> { + // 简化模拟表单 + String doLoginCode = + "fetch(`/sso/doLogin?name=${document.querySelector('#name').value}&pwd=${document.querySelector('#pwd').value}`) " + + " .then(res => res.json()) " + + " .then(res => { if(res.code === 200) { location.reload() } else { alert(res.msg) } } )"; + String res = + "

当前客户端在 SSO-Server 认证中心尚未登录,请先登录

" + + "用户:
" + + "密码:
" + + ""; + return res; }; // 配置:登录处理函数 - ssoServer.doLoginHandle = (name, pwd) -> { - // 此处仅做模拟登录,真实环境应该查询数据进行登录 + ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> { + // 此处仅做模拟登录,真实环境应该查询数据库进行登录 if("sa".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue()); } return SaResult.error("登录失败!"); }; - - // 配置 Http 请求处理器 (在模式三的单点注销功能下用到,如不需要可以注释掉) - ssoServer.sendHttp = url -> { - try { - System.out.println("------ 发起请求:" + url); - String resStr = Forest.get(url).executeAsString(); - System.out.println("------ 请求结果:" + resStr); - return resStr; - } catch (Exception e) { - e.printStackTrace(); - return null; - } - }; } } ``` -注意: -- 在`doLoginHandle`函数里如果要获取name, pwd以外的参数,可通过`SaHolder.getRequest().getParam("xxx")`来获取 -- 在 `sendHttp` 函数中,使用 `try-catch` 是为了提高整个注销流程的容错性,避免在一些极端情况下注销失败(例如:某个 Client 端上线之后又下线,导致 http 请求无法调用成功,从而阻断了整个注销流程) +注意:在`doLoginHandle`函数里如果要获取 name, pwd 以外的参数,可通过`SaHolder.getRequest().getParam("xxx")`来获取。 + 全局异常处理: ``` java @@ -170,25 +166,41 @@ server: # Sa-Token 配置 sa-token: - # ------- SSO-模式一相关配置 (非模式一不需要配置) + # 打印操作日志 + is-log: true + + # SSO-模式一相关配置 (非模式一不需要配置) # cookie: # 配置 Cookie 作用域 # domain: stp.com - # ------- SSO-模式二相关配置 - sso-server: + # SSO-Server 配置 + sso-server: # Ticket有效期 (单位: 秒),默认五分钟 ticket-timeout: 300 - # 所有允许的授权回调地址 - allow-url: "*" - - # ------- SSO-模式三相关配置 (下面的配置在使用SSO模式三时打开) - # 是否打开模式三 - is-http: true - sign: - # API 接口调用秘钥 - secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor - # ---- 除了以上配置项,你还需要为 Sa-Token 配置http请求处理器(文档有步骤说明) + # 应用列表:配置接入的应用信息 + clients: + # 应用 sso-client1:采用模式一对接 (同域、同Redis) + sso-client1: + client: sso-client1 + allow-url: "*" + # 应用 sso-client2:采用模式二对接 (跨域、同Redis) + sso-client2: + client: sso-client2 + allow-url: "*" + secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + # 应用 sso-client3:采用模式三对接 (跨域、跨Redis) + sso-client3: + # 应用名称 + client: sso-client3 + # 允许授权地址 + allow-url: "*" + # 是否接收消息推送 + is-push: true + # 消息推送地址 + push-url: http://sa-sso-client1.com:9003/sso/pushC + # 接口调用秘钥 (如果不配置则使用全局默认秘钥) + secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor spring: # Redis配置 (SSO模式一和模式二使用Redis来同步会话) @@ -201,10 +213,6 @@ spring: port: 6379 # Redis服务器连接密码(默认为空) password: - -forest: - # 关闭 forest 请求日志打印 - log-enabled: false ``` ``` properties @@ -212,25 +220,40 @@ forest: server.port=9000 ################## Sa-Token 配置 ################## -# ------- SSO-模式一相关配置 (非模式一不需要配置) +# 打印操作日志 +sa-token.is-log=true + +# SSO-模式一相关配置 (非模式一不需要配置) # 配置 Cookie 作用域 # sa-token.cookie.domain=stp.com -# ------- SSO-模式二相关配置 +# SSO-Server 配置 # Ticket有效期 (单位: 秒),默认五分钟 sa-token.sso-server.ticket-timeout=300 -# 所有允许的授权回调地址 -sa-token.sso-server.allow-url=* -# ------- SSO-模式三相关配置 (下面的配置在使用SSO模式三时打开) -# 是否打开模式三 -sa-token.sso-server.is-http=true -# API 接口调用秘钥 -sa-token.sign.secret-key=kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +# 应用列表:配置接入的应用信息 +# 应用 sso-client1:采用模式一对接 (同域、同Redis) +sa-token.sso-server.clients.sso-client1.client=sso-client1 +sa-token.sso-server.clients.sso-client1.allow-url=* -# ---- 除了以上配置项,你还需要为 Sa-Token 配置http请求处理器(文档有步骤说明) - -################## Redis配置 (SSO模式一和模式二使用Redis来同步会话) ################## +# 应用 sso-client2:采用模式二对接 (跨域、同Redis) +sa-token.sso-server.clients.sso-client2.client=sso-client2 +sa-token.sso-server.clients.sso-client2.allow-url=* +sa-token.sso-server.clients.sso-client2.secret-key=SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +# 应用 sso-client3:采用模式三对接 (跨域、跨Redis) +# 应用名称 +sa-token.sso-server.clients.sso-client3.client=sso-client3 +# 允许授权地址 +sa-token.sso-server.clients.sso-client3.allow-url=* +# 是否接收消息推送 +sa-token.sso-server.clients.sso-client3.is-push=true +# 消息推送地址 +sa-token.sso-server.clients.sso-client3.push-url=http://sa-sso-client1.com:9003/sso/pushC +# 接口调用秘钥 (如果不配置则使用全局默认秘钥) +sa-token.sso-server.clients.sso-client3.secret-key=SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +# Redis配置 (SSO模式一和模式二使用Redis来同步会话) # Redis数据库索引(默认为0) spring.redis.database=1 # Redis服务器地址 @@ -239,14 +262,11 @@ spring.redis.host=127.0.0.1 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password= - -# 关闭 forest 请求日志打印 -forest.log-enabled: false ``` -注意点:`sa-token.sso-server.allow-url`为了方便测试配置为`*`,线上生产环境一定要配置为详细URL地址 (之后的章节我们会详细阐述此配置项) +注意点:`sa-token.sso-server.clients.xxx.allow-url`为了方便测试配置为`*`,线上生产环境一定要配置为详细 URL 地址 (之后的章节我们会详细阐述此配置项) ### 4、创建启动类 @@ -259,6 +279,8 @@ public class SaSsoServerApplication { System.out.println(); System.out.println("---------------------- Sa-Token SSO 统一认证中心启动成功 ----------------------"); System.out.println("配置信息:" + SaSsoManager.getServerConfig()); + System.out.println("统一认证登录地址:http://sa-sso-server.com:9000/sso/auth"); + System.out.println("测试前需要根据官网文档修改 hosts 文件,测试账号密码:sa / 123456"); System.out.println(); } } @@ -271,7 +293,7 @@ public class SaSsoServerApplication { 访问统一授权地址(仅测试 SSO-Server 是否部署成功,暂时还不需要点击登录): - [http://localhost:9000/sso/auth](http://localhost:9000/sso/auth) -![sso-server-init-login.png](https://oss.dev33.cn/sa-token/doc/sso/sso-server-init-login.png 's-w-sh') +![sso-server-init-login.png](https://oss.dev33.cn/sa-token/doc/sso/sso-server-init-login--v43.png 's-w-sh') 可以看到这个页面目前非常简陋,这是因为我们以上的代码示例,主要目标是为了带大家从零搭建一个可用的SSO认证服务端,所以就对一些不太必要的步骤做了简化。 @@ -280,7 +302,7 @@ public class SaSsoServerApplication { ![sso-server-init-login2.png](https://oss.dev33.cn/sa-token/doc/sso/sso-server-init-login2.png 's-w-sh') 默认账号密码为:`sa / 123456`,先别着急点击登录,因为我们还没有搭建对应的 Client 端项目, -真实项目中我们是不会直接从浏览器访问 `/sso/auth` 授权地址的,我们需要在 Client 端点击登录按钮重定向而来。 +真实项目中我们一般不会直接从浏览器访问 `/sso/auth` 授权地址的,我们需要在 Client 端点击登录按钮重定向而来。 --- diff --git a/sa-token-doc/sso/sso-type1.md b/sa-token-doc/sso/sso-type1.md index 0d858022..7ffcdaad 100644 --- a/sa-token-doc/sso/sso-type1.md +++ b/sa-token-doc/sso/sso-type1.md @@ -58,7 +58,7 @@ sa-token.cookie.domain=stp.com ``` -这个配置原本是被注释掉的,现在将其打开。另外我们格外需要注意: +**这个配置原本是被注释掉的,现在将其打开。**另外我们格外需要注意: 在SSO模式一测试完毕之后,一定要将这个配置再次注释掉,因为模式一与模式二三使用不同的授权流程,这行配置会影响到我们模式二和模式三的正常运行。 @@ -88,10 +88,10 @@ sa-token.cookie.domain=stp.com ${sa.top.version} - + cn.dev33 - sa-token-redis-jackson + sa-token-redis-template ${sa.top.version} @@ -114,8 +114,8 @@ implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' // Sa-Token 插件:整合SSO implementation 'cn.dev33:sa-token-sso:${sa.top.version}' -// Sa-Token 整合 Redis (使用 jackson 序列化方式) -implementation 'cn.dev33:sa-token-redis-jackson:${sa.top.version}' +// Sa-Token 整合 RedisTemplate +implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' implementation 'org.apache.commons:commons-pool2' // Sa-Token插件:权限缓存与业务缓存分离 @@ -136,13 +136,16 @@ public class SsoClientController { // SSO-Client端:首页 @RequestMapping("/") - public String index() { - String authUrl = SaSsoManager.getClientConfig().splicingAuthUrl(); - String solUrl = SaSsoManager.getClientConfig().splicingSloUrl(); - String str = "

Sa-Token SSO-Client 应用端

" + - "

当前会话是否登录:" + StpUtil.isLogin() + "

" + - "

登录 " + - "登录 - " + + "注销 " + + "

"; return str; } @@ -169,10 +172,13 @@ server: sa-token: # SSO-相关配置 sso-client: + # client 标识 + client: sso-client1 # SSO-Server端主机地址 server-url: http://sso.stp.com:9000 - - # 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) + + # 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) + # 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 alone-redis: # Redis数据库索引 database: 1 @@ -192,10 +198,13 @@ server.port=9001 ######### Sa-Token 配置 ######### +# client 标识 +sa-token.sso-client.client=sso-client1 # SSO-Server端主机地址 sa-token.sso-client.server-url=http://sso.stp.com:9000 - -# 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) + +# 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) +# 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 # Redis数据库索引 sa-token.alone-redis.database=1 # Redis服务器地址 @@ -247,9 +256,9 @@ public class SaSso1ClientApplication { 然后点击登录,被重定向至SSO认证中心: -![sso1--login-page2.png](https://oss.dev33.cn/sa-token/doc/sso/sso1--login-page2.png 's-w-sh') +![sso1--login-page2.png](https://oss.dev33.cn/sa-token/doc/sso/sso1--login-page2--v43.png 's-w-sh') -我们点击登录,然后刷新页面: +我们登录之后,然后刷新页面: ![sso1-login-ok.png](https://oss.dev33.cn/sa-token/doc/sso/sso1-login-ok.png 's-w-sh') diff --git a/sa-token-doc/sso/sso-type2.md b/sa-token-doc/sso/sso-type2.md index a996c070..751bf84a 100644 --- a/sa-token-doc/sso/sso-type2.md +++ b/sa-token-doc/sso/sso-type2.md @@ -89,10 +89,10 @@ sa-token.cookie.domain=stp.com ${sa.top.version}
- + cn.dev33 - sa-token-redis-jackson + sa-token-redis-template ${sa.top.version} @@ -106,6 +106,13 @@ sa-token.cookie.domain=stp.com sa-token-alone-redis ${sa.top.version} + + + + cn.dev33 + sa-token-forest + ${sa-token.version} + ``` ``` gradle @@ -115,12 +122,15 @@ implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' // Sa-Token 插件:整合SSO implementation 'cn.dev33:sa-token-sso:${sa.top.version}' -// Sa-Token 整合 Redis (使用 jackson 序列化方式) -implementation 'cn.dev33:sa-token-redis-jackson:${sa.top.version}' +// Sa-Token 整合 RedisTemplate +implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' implementation 'org.apache.commons:commons-pool2' // Sa-Token插件:权限缓存与业务缓存分离 implementation 'cn.dev33:sa-token-alone-redis:${sa.top.version}' + +// Sa-Token插件:整合 Forest 请求工具 +implementation 'cn.dev33:sa-token-forest:${sa.top.version}' ``` @@ -140,28 +150,58 @@ public class SsoClientController { // 首页 @RequestMapping("/") public String index() { - String solUrl = SaSsoManager.getClientConfig().splicingSloUrl(); - String str = "

Sa-Token SSO-Client 应用端

" + - "

当前会话是否登录:" + StpUtil.isLogin() + "

" + - "

登录 " + - "登录 - " + + "单应用注销 - " + + "全端注销 - " + + "账号资料" + + "

"; return str; } /* * SSO-Client端:处理所有SSO相关请求 - * http://{host}:{port}/sso/login -- Client端登录地址,接受参数:back=登录后的跳转地址 - * http://{host}:{port}/sso/logout -- Client端单点注销地址(isSlo=true时打开),接受参数:back=注销后的跳转地址 - * http://{host}:{port}/sso/logoutCall -- Client端单点注销回调地址(isSlo=true时打开),此接口为框架回调,开发者无需关心 + * http://{host}:{port}/sso/login -- Client 端登录地址 + * http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开) + * http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址 */ @RequestMapping("/sso/*") public Object ssoRequest() { return SaSsoClientProcessor.instance.dister(); } + // 配置SSO相关参数 + @Autowired + private void configSso(SaSsoClientTemplate ssoClientTemplate) { + + } + + // 当前应用独自注销 (不退出其它应用) + @RequestMapping("/sso/logoutByAlone") + public Object logoutByAlone() { + StpUtil.logout(); + return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); + } + } ``` +全局异常处理: +``` java +@RestControllerAdvice +public class GlobalExceptionHandler { + // 全局异常拦截 + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } +} +``` + + ##### 3.4、配置SSO认证中心地址 你需要在 `application.yml` 配置如下信息: @@ -170,16 +210,23 @@ public class SsoClientController { ``` yaml # 端口 server: - port: 9001 + port: 9002 # sa-token配置 sa-token: + # 打印操作日志 + is-log: true # SSO-相关配置 sso-client: + # 应用标识 + client: sso-client2 # SSO-Server 端主机地址 server-url: http://sa-sso-server.com:9000 + # API 接口调用秘钥 (单点注销时会用到) + secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor - # 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) + # 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) + # 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 alone-redis: # Redis数据库索引 (默认为0) database: 1 @@ -195,13 +242,20 @@ sa-token: ``` properties # 端口 -server.port=9001 +server.port=9002 ######### Sa-Token 配置 ######### +# 打印操作日志 +sa-token.is-log=true +# 应用标识 +sa-token.sso-client.client=sso-client2 # SSO-Server端 统一认证地址 sa-token.sso-client.server-url=http://sa-sso-server.com:9000 +# API 接口调用秘钥 (单点注销时会用到) +sa-token.sso-client.secret-key=SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor -# 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) +# 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) +# 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 # Redis数据库索引 sa-token.alone-redis.database=1 # Redis服务器地址 @@ -228,9 +282,9 @@ public class SaSso2ClientApplication { System.out.println(); System.out.println("---------------------- Sa-Token SSO 模式二 Client 端启动成功 ----------------------"); System.out.println("配置信息:" + SaSsoManager.getClientConfig()); - System.out.println("测试访问应用端一: http://sa-sso-client1.com:9001"); - System.out.println("测试访问应用端二: http://sa-sso-client2.com:9001"); - System.out.println("测试访问应用端三: http://sa-sso-client3.com:9001"); + System.out.println("测试访问应用端一: http://sa-sso-client1.com:9002"); + System.out.println("测试访问应用端二: http://sa-sso-client2.com:9002"); + System.out.println("测试访问应用端三: http://sa-sso-client3.com:9002"); System.out.println("测试前需要根据官网文档修改hosts文件,测试账号密码:sa / 123456"); System.out.println(); } @@ -241,27 +295,27 @@ public class SaSso2ClientApplication { ### 4、测试访问 -(1) 依次启动 `SSO-Server` 与 `SSO-Client`,然后从浏览器访问:[http://sa-sso-client1.com:9001/](http://sa-sso-client1.com:9001/) +(1) 依次启动 `SSO-Server` 与 `SSO-Client`,然后从浏览器访问:[http://sa-sso-client1.com:9002/](http://sa-sso-client1.com:9002/) - +(注:先前版本文档测试demo端口号为9001,后为了方便区分三种模式改为了9002,因此出现文字描述与截图端口号不一致情况,请注意甄别,后不再赘述) ![sso-client-index.png](https://oss.dev33.cn/sa-token/doc/sso/sso-client-index.png 's-w-sh') (2) 首次打开,提示当前未登录,我们点击 **`登录`** 按钮,页面会被重定向到登录中心 -![sso-server-auth.png](https://oss.dev33.cn/sa-token/doc/sso/sso-server-auth.png 's-w-sh') +![sso-server-auth.png](https://oss.dev33.cn/sa-token/doc/sso/sso-server-auth--v43.png 's-w-sh') -(3) SSO-Server提示我们在认证中心尚未登录,我们点击 **`doLogin登录`** 按钮进行模拟登录 +(3) SSO-Server提示我们在认证中心尚未登录,我们点击 **`登录`** 按钮进行模拟登录 -![sso-server-dologin.png](https://oss.dev33.cn/sa-token/doc/sso/sso-server-dologin.png 's-w-sh') + -(4) SSO-Server认证中心登录成功,我们回到刚才的页面刷新页面 +(4) SSO-Server认证中心登录成功,系统重定向回 client ![sso-client-index-ok.png](https://oss.dev33.cn/sa-token/doc/sso/sso-client-index-ok.png 's-w-sh') (5) 页面被重定向至`Client`端首页,并提示登录成功,至此,`Client1`应用已单点登录成功! -(6) 我们再次访问`Client2`:[http://sa-sso-client2.com:9001/](http://sa-sso-client2.com:9001/) +(6) 我们再次访问`Client2`:[http://sa-sso-client2.com:9002/](http://sa-sso-client2.com:9002/) ![sso-client2-index.png](https://oss.dev33.cn/sa-token/doc/sso/sso-client2-index.png 's-w-sh') @@ -269,7 +323,7 @@ public class SaSso2ClientApplication { ![sso-client2-index-ok.png](https://oss.dev33.cn/sa-token/doc/sso/sso-client2-index-ok.png 's-w-sh') -(8) 同样的方式,我们打开`Client3`,也可以直接登录成功:[http://sa-sso-client3.com:9001/](http://sa-sso-client3.com:9001/) +(8) 同样的方式,我们打开`Client3`,也可以直接登录成功:[http://sa-sso-client3.com:9002/](http://sa-sso-client3.com:9002/) ![sso-client3-index-ok.png](https://oss.dev33.cn/sa-token/doc/sso/sso-client3-index-ok.png 's-w-sh') @@ -291,7 +345,7 @@ public class SaSso2ClientApplication { > - `/sa-token-demo/sa-token-demo-sso2-client/` > > 然后访问: -> - [http://sa-sso-client1.com:9001/](http://sa-sso-client1.com:9001/) +> - [http://sa-sso-client1.com:9002/](http://sa-sso-client1.com:9002/) ![sso-server-login-hua](https://oss.dev33.cn/sa-token/doc/sso/sso-server-login-hua.png 's-w-sh') diff --git a/sa-token-doc/sso/sso-type3.md b/sa-token-doc/sso/sso-type3.md index fb00d8ce..fa752397 100644 --- a/sa-token-doc/sso/sso-type3.md +++ b/sa-token-doc/sso/sso-type3.md @@ -22,64 +22,106 @@ ### 2、在Client 端更改 Ticket 校验方式 +如果想要更直观的感受模式二与模式三的差距,可以把前面章节创建的模式二 demo 代码复制一份,在新复制的项目上继续更改来测试模式三。 -#### 2.1、增加 pom.xml 配置 +#### 2.1、去除 Alone-Redis 依赖 + +模式三要求 sso-client 与 sso-server 连接不同的 redis,所以此处没有必要再引入 sa-token-alone-redis 机制,可以去除相关依赖: -``` xml - +``` xml + - com.dtflys.forest - forest-spring-boot-starter - 1.5.26 + cn.dev33 + sa-token-alone-redis + ${sa.top.version} ``` ``` gradle -// Http请求工具 -implementation 'com.dtflys.forest:forest-spring-boot-starter:1.5.26' +// Sa-Token插件:权限缓存与业务缓存分离 +implementation 'cn.dev33:sa-token-alone-redis:${sa.top.version}' ``` -Forest 是一个轻量级 http 请求工具,详情参考:[Forest](https://forest.dtflyx.com/) +#### 2.2、SSO-Client 端更改配置 -#### 2.2、SSO-Client 端新增配置:API调用秘钥 - -在 `application.yml` 增加: +更改 `application.yml` : ``` yaml -sa-token: - sso-client: - # 打开模式三(使用Http请求校验ticket) +# 端口 +server: + port: 9003 + +# sa-token配置 +sa-token: + # 打印操作日志 + is-log: true + + # sso-client 相关配置 + sso-client: + # 应用标识 + client: sso-client3 + # sso-server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # 使用 Http 请求校验 ticket (模式三) is-http: true - sign: # API 接口调用秘钥 - secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor -forest: - # 关闭 forest 请求日志打印 - log-enabled: false +spring: + # 配置 Redis 连接 (此处与 SSO-Server 端连接不同的 Redis) + redis: + # Redis数据库索引 + database: 3 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s ``` ``` properties -# 打开模式三(使用Http请求校验ticket) -sa-token.sso-client.is-http=true -# 接口调用秘钥 -sa-token.sign.secret-key=kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +# 端口 +server.port=9003 + +# sa-token配置 -# 关闭 forest 请求日志打印 -forest.log-enabled=false +# 打印操作日志 +sa-token.is-log=true + +# sso-client 相关配置 +# 应用标识 +sa-token.sso-client.client=sso-client3 +# sso-server 端主机地址 +sa-token.sso-client.server-url=http://sa-sso-server.com:9000 +# 使用 Http 请求校验 ticket (模式三) +sa-token.sso-client.is-http=true +# API 接口调用秘钥 +sa-token.sso-client.secret-key=SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +# 配置 Redis 连接 (此处与 SSO-Server 端连接不同的 Redis) +# Redis数据库索引 +spring.redis.database=3 +# Redis服务器地址 +spring.redis.host=127.0.0.1 +# Redis服务器连接端口 +spring.redis.port=6379 +# Redis服务器连接密码(默认为空) +spring.redis.password= +# 连接超时时间 +spring.redis.timeout=10s ``` -因为我们已经在控制台手动打印 url 请求日志了,所以此处 `forest.log-enabled=false` 关闭 Forest 框架自身的日志打印,这不是必须的,你可以将其打开。 - -注意 secretkey 秘钥需要与SSO认证中心的一致 - + -#### 2.4、测试 +#### 2.3、测试 重启项目,访问测试: -- [http://sa-sso-client1.com:9001/](http://sa-sso-client1.com:9001/) -- [http://sa-sso-client2.com:9001/](http://sa-sso-client2.com:9001/) -- [http://sa-sso-client3.com:9001/](http://sa-sso-client3.com:9001/) +- [http://sa-sso-client1.com:9003/](http://sa-sso-client1.com:9003/) +- [http://sa-sso-client2.com:9003/](http://sa-sso-client2.com:9003/) +- [http://sa-sso-client3.com:9003/](http://sa-sso-client3.com:9003/) > [!WARNING| label:小提示] > 注:如果已测试运行模式二,可先将Redis中的数据清空,以防旧数据对测试造成干扰 +测试步骤同模式二,不再赘述。 - - + -### 5、无刷单点注销 - -有了单点登录,就必然伴随着单点注销(一处注销,全端下线) - - -此处简单介绍一下 SSO 模式三的单点注销链路过程: - -1. sso-client 端在校验 ticket 时(调用 `http://{sso-server}/sso/checkTicket` 时),将注销回调地址 `http://{sso-client}/sso/logoutCall` 发送到 Server 端。 -2. sso-server 端将此 sso-client 的注销回调地址以 List 集合的形式存储在该账号 Access-Session 的 dataMap。 -3. sso-client 的前端向 sso-client 的后端发起单点注销请求。(调用 `http://{sso-client}/sso/logout`) -4. sso-client 的后端向 sso-server 的后端发送单点注销请求。(调用 `http://{sso-server}/sso/signout`) -5. sso-server 端遍历该账号 Access-Session 存储的注销回调地址集合,逐个通知 sso-client 端下线。(`http://{sso-client}/sso/logoutCall`) -6. sso-server 端注销下线。 -7. sso-server 后端响应 sso-client 后端:注销完成。 -7. sso-client 后端响应 sso-client 前端:注销完成。 -8. 整体完成。 - - - - - -这些逻辑 Sa-Token 内部已经封装完毕,你只需按照文档步骤集成即可。 - -#### 5.1、更改注销方案 - -将 sso-client 首页路由方法里的注销链接换成 `/sso/logout` 接口: -``` java -// SSO-Client端:首页 -@RequestMapping("/") -public String index() { - String str = "

Sa-Token SSO-Client 应用端

" + - "

当前会话是否登录:" + StpUtil.isLogin() + "

" + - "

登录" + - " 注销

"; - return str; -} -``` - -#### 5.2、启动测试 -重启项目,依次登录三个 client: -- [http://sa-sso-client1.com:9001/](http://sa-sso-client1.com:9001/) -- [http://sa-sso-client2.com:9001/](http://sa-sso-client2.com:9001/) -- [http://sa-sso-client3.com:9001/](http://sa-sso-client3.com:9001/) - -![sso-type3-client-index.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type3-client-index.png 's-w-sh') - -在任意一个 client 里,点击 **`[注销]`** 按钮,即可单点注销成功(打开另外两个client,刷新一下页面,登录态丢失)。 - - - -![sso-type3-slo-index.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type3-slo-index.png 's-w-sh') - -PS:这里我们为了方便演示,使用的是超链接跳页面的形式,正式项目中使用 Ajax 调用接口即可做到无刷单点登录退出。 - -例如,我们使用 [Apifox 接口测试工具](https://www.apifox.cn/) 可以做到同样的效果: - -![sso-slo-apifox.png](https://oss.dev33.cn/sa-token/doc/sso/sso-slo-apifox.png 's-w-sh') - -测试完毕! - - - -### 6、后记 +### 3、后记 当我们熟读三种模式的单点登录之后,其实不难发现:所谓单点登录,其本质就是多个系统之间的会话共享。 当我们理解这一点之后,三种模式的工作原理也浮出水面: diff --git a/sa-token-doc/sso/user-data-sync.md b/sa-token-doc/sso/user-data-sync.md index 84e39e8c..62584994 100644 --- a/sa-token-doc/sso/user-data-sync.md +++ b/sa-token-doc/sso/user-data-sync.md @@ -102,12 +102,12 @@ ``` java // 配置SSO相关参数 @Autowired -private void configSso(SaSsoServerConfig ssoServer) { +private void configSso(SaSsoServerTemplate ssoServerTemplate) { // 其它配置 ... // 配置:Ticket校验函数 - ssoServer.checkTicketAppendData = (loginId, result) -> { + ssoServerTemplate.strategy.checkTicketAppendData = (loginId, result) -> { System.out.println("-------- 追加返回信息到 sso-client --------"); // 在校验 ticket 后,给 sso-client 端追加返回信息的函数 @@ -126,12 +126,12 @@ private void configSso(SaSsoServerConfig ssoServer) { ``` java // 配置SSO相关参数 @Autowired -private void configSso(SaSsoClientConfig ssoClient) { +private void configSso(SaSsoClientTemplate ssoClientTemplate) { // 其它配置 ... // 自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用) - ssoClient.ticketResultHandle = (ctr, back) -> { + ssoClientTemplate.strategy.ticketResultHandle = (ctr, back) -> { System.out.println("--------- 自定义 ticket 校验结果处理函数 ---------"); System.out.println("此账号在 sso-server 的 userId:" + ctr.loginId); System.out.println("此账号在 sso-server 会话剩余有效期:" + ctr.remainSessionTimeout + " 秒"); @@ -192,12 +192,12 @@ private void configSso(SaSsoClientConfig ssoClient) { ``` java // 配置SSO相关参数 @Autowired -private void configSso(SaSsoClientConfig ssoClient) { +private void configSso(SaSsoClientTemplate ssoClientTemplate) { // 其它配置 ... // 自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用) - ssoClient.ticketResultHandle = (ctr, back) -> { + ssoClientTemplate.strategy.ticketResultHandle = (ctr, back) -> { System.out.println("--------- 自定义 ticket 校验结果处理函数 ---------"); System.out.println("此账号在 sso-server 的 userId:" + ctr.loginId); System.out.println("此账号在 sso-server 会话剩余有效期:" + ctr.remainSessionTimeout + " 秒"); @@ -240,3 +240,66 @@ private void configSso(SaSsoClientConfig ssoClient) { > - 5.1 查的到,证明有账号,直接登录。 > - 5.2 查不到,证明无账号,程序自动给他添加一条 user 账号,并登录。 > 6. 登录完成。 + + + +### 5、解决模式三下,loginId 与 centerId 不一致的问题 + +按照字段关联法登录之后,如果一个用户在本地应用端的 userId 和认证中心端的 userId 不一致,则可能发生单点注销失败的情况: + +假设,一个用户在认证中心的 userId=10002,在本地应用端的 userId=100335, +则在本地应用端发起单点注销时,其传递的 loginId 值是 100335,在 sso-server 是找不到 userId=100335 用户的,自然无法单点注销成功。 + +解决方案是在本地应用端重写 loginId 与 centerId 转换策略函数,做到本地应用 userId 与认证中心 userId 的互相映射: + +``` java +@RestController +public class SsoClientController { + + // 配置SSO相关参数 + @Autowired + private void configSso(SaSsoClientTemplate ssoClientTemplate) { + // 重写 loginId 与 centerId 转换策略函数,做到本地应用 userId 与认证中心 userId 的互相映射 + + // 将 centerId 转换为 loginId 的函数 + ssoClientTemplate.strategy.convertCenterIdToLoginId = (centerId) -> { + return "Stu" + centerId; + }; + // 将 loginId 转换为 centerId 的函数 + ssoClientTemplate.strategy.convertLoginIdToCenterId = (loginId) -> { + return loginId.toString().substring(3); + }; + } + +} +``` + +如上代码,演示了应用本地 loginId 与认证中心 centerId 不一致时的转换写法(演示的逻辑为添加和裁剪指定前缀),真实项目中,应该根据用户表存储的映射关系来做查询返回。 + +值得注意的是,在重写转换策略后,我们在消息推送时也应该严格按照转换写法提交 loginId 参数,例如: + +``` java +// 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 +@RequestMapping("/sso/myInfo") +public Object myInfo() { + // 如果尚未登录 + if( ! StpUtil.isLogin()) { + return "尚未登录,无法获取"; + } + + // 原写法:直接调用 StpUtil.getLoginId() 当做 centerId 来提交 + // Object centerId = StpUtil.getLoginId(); + + // 新写法:获取本地 loginId 对应的认证中心 centerId + Object centerId = SaSsoClientUtil.getSsoTemplate().strategy.convertLoginIdToCenterId.run(StpUtil.getLoginId()); + + // 推送消息 + SaSsoMessage message = new SaSsoMessage(); + message.setType("userinfo"); + message.set("loginId", centerId); + SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); + + // 返回给前端 + return result; +} +``` \ No newline at end of file diff --git a/sa-token-doc/static/doc.css b/sa-token-doc/static/doc.css index 497d5cd4..cc648791 100644 --- a/sa-token-doc/static/doc.css +++ b/sa-token-doc/static/doc.css @@ -207,6 +207,8 @@ body{ .lang-html .token.tag .attr-name *{color: #A6E22E; opacity: 0.9;} .lang-html .token.tag .attr-value, .lang-html .token.tag .attr-value *{color: #E6DB74; opacity: 0.9;} +.lang-html .token.annotation.punctuation{color: #ddd;} +.lang-html .token.punctuation{color: #ddd;} /* java语言样式优化 */ .main-box .lang-java{color: #01a252 !important;; opacity: 1;}