docs: 完善 SSO 章节文档

This commit is contained in:
click33
2025-05-08 20:52:52 +08:00
parent 88f99c49fb
commit 5d3b1f4455
20 changed files with 1391 additions and 463 deletions

View File

@@ -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)
<!-- - [不同 Client 不同秘钥](/sso/sso-diff-key) -->
- [用户数据同步 / 迁移](/sso/user-data-sync)
- [NoSdk、ReSdk 模式与非 java 项目](/sso/sso-nosdk)
- [SSO 代码 API 参考](/sso/sso-dev)
- [常见问题总结](/sso/sso-questions)
- [Sa-Sso-Pro单点登录商业版](/sso/sso-pro)

View File

@@ -2,4 +2,4 @@
<!-- 参考:[https://blog.csdn.net/shengzhang_/article/details/119928794](https://blog.csdn.net/shengzhang_/article/details/119928794) -->
参考:[https://juejin.cn/post/7247376558367981627](https://juejin.cn/post/7247376558367981627)
参考:[https://juejin.cn/post/7491603065944129590](https://juejin.cn/post/7491603065944129590)

View File

@@ -0,0 +1,108 @@
# 匿名 Client 接入
匿名 Client 就是指在客户端没有配置 `sso-client` 的应用,没有一个明确的 “Client” 标识名称。
匿名 Client 在一些关键步骤中不会构建 `client` 参数,如:“重定向至认证中心授权地址”、“校验 ticket”、“单点注销” 等。
要想匿名 client 接入,你需要做一些特殊配置。
### 1、在 sso-server 端开启匿名 client 接入
开启方式一,通过配置项方式:
<!---------------------------- tabs:start ---------------------------->
<!------------- tab:yaml 风格 ------------->
``` yaml
# Sa-Token 配置
sa-token:
# SSO-Server 配置
sso-server:
# 是否启用匿名 client (开启匿名 client 后,允许客户端接入时不提交 client 参数)
allow-anon-client: true
# 所有允许的授权回调地址 (匿名 client 使用)
allow-url: "*"
# API 接口调用秘钥 (全局默认 + 匿名 client 使用)
secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
```
<!------------- tab:properties 风格 ------------->
``` 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
```
<!---------------------------- tabs:end ---------------------------->
开启方式二,通过代码重写方式:
``` 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 端有了备案,也就可以成功通知此应用发起单点注销掉了。
如果当前应用不需要单点注销可以不配置此字段。

View File

@@ -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、应用端调用消息推送接口获取数据
首先保证在配置文件里要配置上消息推送的具体地址
<!---------------------------- tabs:start ---------------------------->
<!------------- tab:yaml 风格 ------------->
``` 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
```
<!------------- tab:properties 风格 ------------->
``` 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
```
<!---------------------------- tabs:end ---------------------------->
然后在需要拉取资料的地方:
``` 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;
}
```

158
sa-token-doc/sso/signout.md Normal file
View File

@@ -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. 整体完成。
<button class="show-img" img-src="https://oss.dev33.cn/sa-token/doc/g/g3--sso3-logout.gif">加载动态演示图</button>
这些逻辑 Sa-Token 内部已经封装完毕,你只需按照文档步骤集成即可。以模式三 demo 为例:
#### 2.1、更改注销方案
单点注销是 Sa-Token SSO 内部已封装的接口,无需手动再添加,只需要在前端调用即可。
``` java
// SSO-Client端首页
@RequestMapping("/")
public String index() {
String str = "<h2>Sa-Token SSO-Client 应用端 (模式三)</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")</p>" +
"<p> " +
"<a href='/sso/login?back=/'>登录</a> - " +
"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - " +
"<a href='/sso/logout?back=self'>全端注销</a> " +
"</p>";
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.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type3-slo.png 's-w-sh') -->
![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 = "<h2>Sa-Token SSO-Client 应用端 (模式三)</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")</p>" +
"<p> " +
"<a href='/sso/login?back=/'>登录</a> - " +
"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - " +
"<a href='/sso/logout?back=self&singleDeviceIdLogout=true'>单浏览器注销</a> - " +
"<a href='/sso/logout?back=self'>全端注销</a> " +
"</p>";
return str;
}
```
重点在第 9 行。
> [!WARNING| label:测试注意点]
> 在进行测试时,同时将一个浏览器双击打开两次,是不算 “不同浏览器” 的,虽然你打开了两个浏览器窗口,但是这两个浏览器的会话数据是互通的。
>
> 必须打开两个不同的浏览器来测试,或者按快捷键 `ctrl + shift + N` 打开隐私模式,才可以做到会话相互隔离。

View File

@@ -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值}&timestamp={13位时间戳}&key={secretkey秘钥} )` 注:[]内容代表可选 |
| sign | 是 | 签名,生成算法:`md5( client={client值}&nonce={随机字符串}&timestamp={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={随机字符串}&timestamp={13位时间戳}&key={secretkey秘钥} )` |
| client | 否 | 客户端标识,可不填,一般在帮助 “sso-server 端不同client不同秘钥” 的场景下找到对应秘钥时,才填写 |
例如:
``` url
http://{host}:{port}/sso/signout?loginId={value}&timestamp={value}&nonce={value}&sign={value}
```
将返回 json 数据结果,形如:
``` js
{
"code": 200, // 200表示请求成功非200标识请求失败
"msg": "单点注销成功",
"data": null
}
```
如果单点注销失败,将返回:
``` js
{
"code": 500, // 200表示请求成功非200标识请求失败
"msg": "签名无效xxx", // 失败原因
"data": null
}
```
<br>
@@ -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={随机字符串}&timestamp={13位时间戳}&key={secretkey秘钥} )` |
此接口可根据消息类型增加任意参数。新增加的参数要参与 sign 签名。
返回值示例:
- 推送成功时:
``` js
{
"code": 200,
"msg": "ok",
"data": "10001", // 返回的数据
}
```
- 推送失败时:
``` js
{
"code": 500, // 200表示请求成功非200标识请求失败
"msg": "签名无效xxx", // 失败原因
"data": null
}
```

View File

@@ -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 将无法单点登录成功
为了方便测试,上述代码将其配置为`*`,但是,<font color="#FF0000" >在生产环境中,此配置项绝对不能配置为 * </font>,否则会有被 Ticket 劫持的风险
为了方便测试,上述代码将其配置为`*`,但是,<font color="#FF0000" >在生产环境中,此配置项绝对不能配置为 * </font>,否则会有被 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
```
<!------------- tab:properties 风格 ------------->
``` 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
```
<!---------------------------- tabs:end ---------------------------->

View File

@@ -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();
}
// ... 其它方法
}
```

View File

@@ -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 标识:
<!---------------------------- tabs:start ---------------------------->
@@ -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)) {

143
sa-token-doc/sso/sso-dev.md Normal file
View File

@@ -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<T> 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
// 构建 URLsso-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
// 构建URLServer端 单点登录授权地址
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)

View File

@@ -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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sa-Token-SSO-Client端-测试页(前后端分离版)</title>
<title>Sa-Token-SSO-Client端-测试页(前后端分离版-原生h5</title>
</head>
<body>
<h2>Sa-Token SSO-Client 应用端(前后端分离版)</h2>
<h2>Sa-Token SSO-Client 应用端(前后端分离版-原生h5</h2>
<p>当前是否登录:<b class="is-login"></b></p>
<p>
<a href="javascript:location.href='sso-login.html?back=' + encodeURIComponent(location.href);">登录</a>
<a href="javascript:location.href=baseUrl + '/sso/logout?satoken=' + localStorage.satoken + '&back=' + encodeURIComponent(location.href);">注销</a>
<a href="javascript: login();">登录</a> -
<a href="javascript: doLogoutByAlone();">单应用注销</a> -
<a href="javascript: doLogoutBySingleDeviceId();">单浏览器注销</a> -
<a href="javascript: doLogout();">全端注销</a> -
<a href="javascript: doMyInfo();">账号资料</a>
</p>
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
<script src="sso-common.js"></script>
<script type="text/javascript">
// 后端接口地址
var baseUrl = "http://sa-sso-client1.com:9001";
// 查询当前会话是否登录
$.ajax({
url: baseUrl + '/sso/isLogin',
type: "post",
dataType: 'json',
headers: {
"X-Requested-With": "XMLHttpRequest",
"satoken": localStorage.getItem("satoken")
},
success: function(res){
$('.is-login').html(res.data + '');
},
error: function(xhr, type, errorThrown){
return alert("异常:" + JSON.stringify(xhr));
}
});
// 登录
function login() {
location.href = 'sso-login.html?back=' + encodeURIComponent(location.href);
}
// 单应用注销
function doLogoutByAlone() {
ajax('/sso/logoutByAlone', {}, function(res){
doIsLogin();
})
}
// 单浏览器注销
function doLogoutBySingleDeviceId() {
ajax('/sso/logout', { singleDeviceIdLogout: true }, function(res){
doIsLogin();
})
}
// 全端注销
function doLogout() {
ajax('/sso/logout', { }, function(res){
doIsLogin();
})
}
// 账号资料
function doMyInfo() {
ajax('/sso/myInfo', { }, function(res){
alert(JSON.stringify(res));
})
}
// 判断是否登录
function doIsLogin() {
ajax('/sso/isLogin', {}, function(res){
if(res.data) {
setHtml('.is-login', res.data + ' (' + res.loginId + ')');
} else {
setHtml('.is-login', res.data);
}
})
}
doIsLogin();
</script>
</body>
</html>
```
### 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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sa-Token-SSO-Client端-登录中转页页</title>
<style type="text/css">
</style>
</head>
<body>
<div class="login-box">
加载中 ...
</div>
<script src="sso-common.js"></script>
<script type="text/javascript">
var back = getParam('back', '/');
var ticket = getParam('ticket');
window.onload = function(){
if(ticket) {
doLoginByTicket(ticket);
} else {
goSsoAuthUrl();
}
}
// 重定向至认证中心
function goSsoAuthUrl() {
ajax('/sso/getSsoAuthUrl', {clientLoginUrl: location.href}, function(res) {
location.href = res.data;
})
}
// 根据ticket值登录
function doLoginByTicket(ticket) {
ajax('/sso/doLoginByTicket', {ticket: ticket}, function(res) {
localStorage.setItem('satoken', res.data);
location.href = decodeURIComponent(back);
})
}
</script>
</body>
</html>
```
### 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` ,例如:
<!---------------------------- tabs:start ---------------------------->
<!------------- tab:yaml 风格 ------------->
``` 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
```
<!------------- tab:properties 风格 ------------->
``` 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
```
<!---------------------------- tabs:end ---------------------------->
然后我们启动项目 `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 端的授权登录。

View File

@@ -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` 字段
<!---------------------------- tabs:start ---------------------------->
<!------------- tab:yaml 风格 ------------->
``` yaml
# Sa-Token 配置
sa-token:
# SSO-Server 配置
sso-server:
# 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址
home-route: /home
```
<!------------- tab:properties 风格 ------------->
``` properties
# 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址
sa-token.sso-server.home-route: /home
```
<!---------------------------- tabs:end ---------------------------->
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 = "<h2>SSO 平台首页</h2>";
String client1 = "<p><a href='" + link1 + "' target='_blank'> 进入Client1系统 </a></p>";
String client2 = "<p><a href='" + link2 + "' target='_blank'> 进入Client2系统 </a></p>";
String client3 = "<p><a href='" + link3 + "' target='_blank'> 进入Client3系统 </a></p>";
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 = "<h2>SSO 平台首页 (平台中心模式)</h2>";
String client1 = "<p><a href='" + link1 + "' target='_blank'> 进入Client1系统 </a></p>";
String client2 = "<p><a href='" + link2 + "' target='_blank'> 进入Client2系统 </a></p>";
String client3 = "<p><a href='" + link3 + "' target='_blank'> 进入Client3系统 </a></p>";
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` 登录页,我们登录上之后,便会跳转到平台中心首页:

View File

@@ -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 模式作为参考。

View File

@@ -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<SaSsoClientModel> 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() {

View File

@@ -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 依赖的基础上,继续引入
<!---------------------------- tabs:start ---------------------------->
<!-------- tab:Maven 方式 -------->
@@ -27,10 +27,10 @@
<version>${sa.top.version}</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<!-- Sa-Token 插件:整合 RedisTemplate -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<artifactId>sa-token-redis-template</artifactId>
<version>${sa.top.version}</version>
</dependency>
<dependency>
@@ -44,11 +44,11 @@
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Http请求工具(在模式三的单点注销功能下用到,如不需要可以注释掉) -->
<!-- Sa-Token 插件:整合 Forest 请求工具 (模式三需要通过 http 请求推送消息) -->
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-spring-boot-starter</artifactId>
<version>1.5.26</version>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-forest</artifactId>
<version>${sa.top.version}</version>
</dependency>
```
<!-------- tab:Gradle 方式 -------->
@@ -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'
```
<!---------------------------- tabs:end ---------------------------->
@@ -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端尚未登录请先访问"
+ "<a href='/sso/doLogin?name=sa&pwd=123456' target='_blank'> doLogin登录 </a>"
+ "进行登录之后,刷新页面开始授权";
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 =
"<h2>当前客户端在 SSO-Server 认证中心尚未登录,请先登录</h2>" +
"用户:<input id='name' /> <br> " +
"密码:<input id='pwd' /> <br>" +
"<button onclick=\"" + doLoginCode + "\">登录</button>";
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")`来获取。
<!-- - `deviceId` 参数代表登录端设备id是为了后续的 “单设备注销” 功能做准备,如果不需要此功能可以省略此参数。 -->
全局异常处理:
``` 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
```
<!------------- tab:properties 风格 ------------->
``` 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
```
<!---------------------------- tabs:end ---------------------------->
注意点:`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 端点击登录按钮重定向而来。
---

View File

@@ -58,7 +58,7 @@ sa-token.cookie.domain=stp.com
```
<!---------------------------- tabs:end ---------------------------->
这个配置原本是被注释掉的,现在将其打开。另外我们格外需要注意:
**这个配置原本是被注释掉的,现在将其打开。**另外我们格外需要注意:
在SSO模式一测试完毕之后一定要将这个配置再次注释掉因为模式一与模式二三使用不同的授权流程这行配置会影响到我们模式二和模式三的正常运行。
@@ -88,10 +88,10 @@ sa-token.cookie.domain=stp.com
<version>${sa.top.version}</version>
</dependency>
<!-- Sa-Token 整合redis (使用jackson序列化方式) -->
<!-- Sa-Token 整合 RedisTemplate -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<artifactId>sa-token-redis-template</artifactId>
<version>${sa.top.version}</version>
</dependency>
<dependency>
@@ -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 = "<h2>Sa-Token SSO-Client 应用端</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
"<p><a href=\"javascript:location.href='" + authUrl + "?mode=simple&redirect=' + encodeURIComponent(location.href);\">登录</a> " +
"<a href=\"javascript:location.href='" + solUrl + "?back=' + encodeURIComponent(location.href);\">注销</a> </p>";
public String index(HttpServletRequest request) {
String url = SaFoxUtil.encodeUrl( SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), request.getQueryString()) );
SaSsoClientConfig cfg = SaSsoManager.getClientConfig();
String str = "<h2>Sa-Token SSO-Client 应用端 (模式一)</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")</p>" +
"<p>" +
"<a href='" + cfg.splicingAuthUrl() + "?mode=simple&client=" + cfg.getClient() + "&redirect=" + url + "'>登录</a> - " +
"<a href='" + cfg.splicingSignoutUrl() + "?back=" + url + "'>注销</a> " +
"</p>";
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')

View File

@@ -89,10 +89,10 @@ sa-token.cookie.domain=stp.com
<version>${sa.top.version}</version>
</dependency>
<!-- Sa-Token 整合redis (使用jackson序列化方式) -->
<!-- Sa-Token 整合 RedisTemplate -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<artifactId>sa-token-redis-template</artifactId>
<version>${sa.top.version}</version>
</dependency>
<dependency>
@@ -106,6 +106,13 @@ sa-token.cookie.domain=stp.com
<artifactId>sa-token-alone-redis</artifactId>
<version>${sa.top.version}</version>
</dependency>
<!-- Sa-Token 插件:整合 Forest 请求工具 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-forest</artifactId>
<version>${sa-token.version}</version>
</dependency>
```
<!-------- tab:Gradle 方式 -------->
``` 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}'
```
<!---------------------------- tabs:end ---------------------------->
@@ -140,28 +150,58 @@ public class SsoClientController {
// 首页
@RequestMapping("/")
public String index() {
String solUrl = SaSsoManager.getClientConfig().splicingSloUrl();
String str = "<h2>Sa-Token SSO-Client 应用端</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
"<p><a href=\"javascript:location.href='/sso/login?back=' + encodeURIComponent(location.href);\">登录</a> " +
"<a href=\"javascript:location.href='" + solUrl + "?back=' + encodeURIComponent(location.href);\">注销</a> </p>";
String str = "<h2>Sa-Token SSO-Client 应用端 (模式二)</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")</p>" +
"<p> " +
"<a href='/sso/login?back=/'>登录</a> - " +
"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - " +
"<a href='/sso/logout?back=self'>全端注销</a> - " +
"<a href='/sso/myInfo' target='_blank'>账号资料</a>" +
"</p>";
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:
<!------------- tab:properties 风格 ------------->
``` 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因此出现文字描述与截图端口号不一致情况请注意甄别后不再赘述 -->
先前版本文档测试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')
<!-- ![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')

View File

@@ -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 机制,可以去除相关依赖:
<!---------------------------- tabs:start ---------------------------->
<!-------- tab:Maven 方式 -------->
``` xml
<!-- Http请求工具 -->
``` xml
<!-- Sa-Token插件权限缓存与业务缓存分离 -->
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-spring-boot-starter</artifactId>
<version>1.5.26</version>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-alone-redis</artifactId>
<version>${sa.top.version}</version>
</dependency>
```
<!-------- tab:Gradle 方式 -------->
``` 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}'
```
<!---------------------------- tabs:end ---------------------------->
Forest 是一个轻量级 http 请求工具,详情参考:[Forest](https://forest.dtflyx.com/)
#### 2.2、SSO-Client 端更改配置
#### 2.2、SSO-Client 端新增配置API调用秘钥
在 `application.yml` 增加:
更改 `application.yml`
<!---------------------------- tabs:start ---------------------------->
<!------------- tab:yaml 风格 ------------->
``` 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
```
<!------------- tab:properties 风格 ------------->
``` 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
```
<!---------------------------- tabs:end ---------------------------->
因为我们已经在控制台手动打印 url 请求日志了,所以此处 `forest.log-enabled=false` 关闭 Forest 框架自身的日志打印,这不是必须的,你可以将其打开。
注意 secretkey 秘钥需要与SSO认证中心的一致
<!--
#### 2.3、SSO-Client 配置 http 请求处理器
``` java
// 配置SSO相关参数
@@ -93,24 +135,24 @@ private void configSso(SaSsoClientConfig ssoClient) {
return resStr;
};
}
```
``` -->
#### 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中的数据清空以防旧数据对测试造成干扰
测试步骤同模式二,不再赘述。
<!--
### 3、获取 UserInfo
除了账号id我们可能还需要将用户的昵称、头像等信息从 Server端 带到 Client端用户资料的拉取。
@@ -252,71 +294,10 @@ public Object getFansList(Long loginId) {
#### 4.3、访问测试
访问测试:[http://sa-sso-client1.com:9001/sso/myFansList](http://sa-sso-client1.com:9001/sso/myFansList)
-->
### 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. 整体完成。
<button class="show-img" img-src="https://oss.dev33.cn/sa-token/doc/g/g3--sso3-logout.gif">加载动态演示图</button>
这些逻辑 Sa-Token 内部已经封装完毕,你只需按照文档步骤集成即可。
#### 5.1、更改注销方案
将 sso-client 首页路由方法里的注销链接换成 `/sso/logout` 接口:
``` java
// SSO-Client端首页
@RequestMapping("/")
public String index() {
String str = "<h2>Sa-Token SSO-Client 应用端</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
"<p><a href=\"javascript:location.href='/sso/login?back=' + encodeURIComponent(location.href);\">登录</a>" +
" <a href='/sso/logout?back=self'>注销</a></p>";
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.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type3-slo.png 's-w-sh') -->
![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、后记
当我们熟读三种模式的单点登录之后,其实不难发现:所谓单点登录,其本质就是多个系统之间的会话共享。
当我们理解这一点之后,三种模式的工作原理也浮出水面:

View File

@@ -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;
}
```

View File

@@ -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;}