Sa-Token 单点登录第三篇:跨域、同 Redis 下的 SSO 对接步骤
大家好,由于单篇博客长度有限,我将针对 SSO 单点登录 开启一个系列文章,此为第三篇,想完整的了解 SSO 技术的同学欢迎点开我的账号动态参考本系列历史文章。
Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
Gitee 开源地址:https://gitee.com/dromara/sa-token
凡是稍微上点规模的系统,统一认证中心都是绕不过去的槛。而单点登录 —— 便是我们搭建统一认证中心的关键。
如果我们的多个系统:部署在不同的域名之下,但是后端可以连接同一个Redis,那么便可以使用 SSO 模式二:[URL重定向传播会话] 的方式做到单点登录。
一、设计思路
首先我们再次复习一下,多个系统之间为什么无法同步登录状态?
- 前端的
Token无法在多个系统下共享。 - 后端的
Session无法在多个系统间共享。
关于第二点,我们已在之前的章节 "SSO模式一" 章节中阐述,使用 Alone独立Redis插件 做到权限缓存直连 SSO-Redis 数据中心,在此不再赘述。
而第一点,才是我们解决问题的关键所在,在跨域模式下,意味着 "共享Cookie方案" 的失效,我们必须采用一种新的方案来传递Token。
- 用户在 子系统 点击
[登录]按钮。 - 用户跳转到子系统登录接口
/sso/login,并携带back参数记录初始页面URL。- 形如:
http://{sso-client}/sso/login?back=xxx
- 形如:
- 子系统检测到此用户尚未登录,再次将其重定向至SSO认证中心,并携带
redirect参数记录子系统的登录页URL。- 形如:
http://{sso-server}/sso/auth?redirect=xxx?back=xxx
- 形如:
- 用户进入了 SSO认证中心 的登录页面,开始登录。
- 用户 输入账号密码 并 登录成功,SSO认证中心再次将用户重定向至子系统的登录接口
/sso/login,并携带ticket码参数。- 形如:
http://{sso-client}/sso/login?back=xxx&ticket=xxxxxxxxx
- 形如:
- 子系统根据
ticket码从SSO-Redis中获取账号id,并在子系统登录此账号会话。 - 子系统将用户再次重定向至最初始的
back页面。
整个过程,除了第四步用户在SSO认证中心登录时会被打断,其余过程均是自动化的,当用户在另一个子系统再次点击[登录]按钮,由于此用户在SSO认证中心已有会话存在,
所以第四步也将自动化,也就是单点登录的最终目的 —— 一次登录,处处通行。
动态演示图:

下面我们按照步骤依次完成上述过程:
二、准备工作
首先修改hosts文件(C:\windows\system32\drivers\etc\hosts),添加以下IP映射,方便我们进行测试:
127.0.0.1 sa-sso-server.com
127.0.0.1 sa-sso-client1.com
127.0.0.1 sa-sso-client2.com
127.0.0.1 sa-sso-client3.com- 1
- 2
- 3
- 4
三、搭建 Client 端项目
搭建示例在 Sa-Token 官方仓库的
/sa-token-demo/sa-token-demo-sso2-client/,如遇到难点可结合源码进行测试学习
3.1、去除 SSO-Server 的 Cookie 作用域配置
在SSO模式一章节中我们打开了配置:
sa-token:
cookie:
# 配置 Cookie 作用域
domain: stp.com - 1
- 2
- 3
- 4
此为模式一专属配置,现在我们将其注释掉
3.2、创建 SSO-Client 端项目
创建一个 SpringBoot 项目 sa-token-demo-sso2-client,引入依赖:
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.com -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.34.0</version>
</dependency>
<!-- Sa-Token 插件:整合SSO -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso</artifactId>
<version>1.34.0</version>
</dependency>
<!-- Sa-Token 整合redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.34.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Sa-Token插件:权限缓存与业务缓存分离 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-alone-redis</artifactId>
<version>1.34.0</version>
</dependency>- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
Gradle 方式:
// Sa-Token 权限认证,在线文档:https://sa-token.com
implementation 'cn.dev33:sa-token-spring-boot-starter:1.34.0'
// Sa-Token 插件:整合SSO
implementation 'cn.dev33:sa-token-sso:1.34.0'
// Sa-Token 整合 Redis (使用 jackson 序列化方式)
implementation 'cn.dev33:sa-token-dao-redis-jackson:1.34.0'
implementation 'org.apache.commons:commons-pool2'
// Sa-Token插件:权限缓存与业务缓存分离
implementation 'cn.dev33:sa-token-alone-redis:1.34.0'- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
3.3、创建 SSO-Client 端认证接口
同 SSO-Server 一样,Sa-Token 为 SSO-Client 端所需代码也提供了完整的封装,你只需提供一个访问入口,接入 Sa-Token 的方法即可。
/**
* Sa-Token-SSO Client端 Controller
*/
@RestController
public class SsoClientController {
// 首页
@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;
}
/*
* 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时打开),此接口为框架回调,开发者无需关心
*/
@RequestMapping("/sso/*")
public Object ssoRequest() {
return SaSsoProcessor.instance.clientDister();
}
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
3.4、配置SSO认证中心地址
你需要在 application.yml 配置如下信息:
# 端口
server:
port: 9001
# sa-token配置
sa-token:
# SSO-相关配置
sso:
# SSO-Server端 统一认证地址
auth-url: http://sa-sso-server.com:9000/sso/auth
# 是否打开单点注销接口
is-slo: true
# 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis)
alone-redis:
# Redis数据库索引 (默认为0)
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接超时时间
timeout: 10s- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
properties 风格:
# 端口
server.port=9001
######### Sa-Token 配置 #########
# SSO-Server端 统一认证地址
sa-token.sso.auth-url=http://sa-sso-server.com:9000/sso/auth
# 是否打开单点注销接口
sa-token.sso.is-slo=true
# 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis)
# Redis数据库索引
sa-token.alone-redis.database=1
# Redis服务器地址
sa-token.alone-redis.host=127.0.0.1
# Redis服务器连接端口
sa-token.alone-redis.port=6379
# Redis服务器连接密码(默认为空)
sa-token.alone-redis.password=
# 连接超时时间
sa-token.alone-redis.timeout=10s- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
注意点:sa-token.alone-redis 的配置需要和SSO-Server端连接同一个Redis(database也要一样)
3.5、写启动类
@SpringBootApplication
public class SaSso2ClientApplication {
public static void main(String[] args) {
SpringApplication.run(SaSso2ClientApplication.class, args);
System.out.println("\nSa-Token-SSO Client端启动成功");
}
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
启动项目
四、测试访问
(1) 依次启动 SSO-Server 与 SSO-Client,然后从浏览器访问:http://sa-sso-client1.com:9001/

(2) 首次打开,提示当前未登录,我们点击登录 按钮,页面会被重定向到登录中心

(3) SSO-Server提示我们在认证中心尚未登录,我们点击 doLogin登录按钮进行模拟登录

(4) SSO-Server认证中心登录成功,我们回到刚才的页面刷新页面

(5) 页面被重定向至Client端首页,并提示登录成功,至此,Client1应用已单点登录成功!
(6) 我们再次访问Client2:http://sa-sso-client2.com:9001/

(7) 提示未登录,我们点击登录按钮,会直接提示登录成功

(8) 同样的方式,我们打开Client3,也可以直接登录成功:http://sa-sso-client3.com:9001/

至此,测试完毕!
可以看出,除了在Client1端我们需要手动登录一次之外,在Client2端和Client3端都是可以无需再次认证,直接登录成功的。
我们可以通过 F12控制台 Network 跟踪整个过程

五、Ticket 劫持攻击
在上面的示例中,配置项 sa-token.sso.allow-url=* 意为配置所有允许的Client端授权地址,不在此配置项中的URL将无法单点登录成功。
为了方便测试,上述代码将其配置为*,但是,在生产环境中,此配置项绝对不能配置为 * ,否则会有被 Ticket 劫持的风险。
假设攻击者根据模仿我们的授权地址,巧妙的构造一个URL:
http://sa-sso-server.com:9000/sso/auth?redirect=https://www.baidu.com/
当不知情的小红被诱导访问了这个URL时,它将被重定向至百度首页

可以看到,代表着用户身份的 Ticket 码也显现到了URL之中,借此漏洞,攻击者完全可以构建一个URL将小红的 Ticket 码自动提交到攻击者自己的服务器,伪造小红身份登录网站
六、防范方法
造成T icket 劫持攻击的直接原因就是SSO-Server认证中心没有对 redirect地址 进行任何的限制,防范的方法也很简单,就是对redirect参数进行校验,如果其不在指定的URL列表中时,拒绝下放ticket
我们将其配置为一个具体的URL:
sa-token:
sso:
# 配置允许单点登录的 url
allow-url: http://sa-sso-client1.com:9001/sso/login- 1
- 2
- 3
- 4
# 配置允许单点登录的 url
sa-token.sso.allow-url=http://sa-sso-client1.com:9001/sso/login- 1
- 2
再次访问上述链接:

域名没有通过校验,拒绝授权!
配置安全性参考表
| 配置方式 | 举例 | 安全性 | 建议 |
|---|---|---|---|
| 配置为* | * |
低 | 禁止在生产环境下使用 |
| 配置到域名 | http://sa-sso-client1.com/* |
中 | 不建议在生产环境下使用 |
| 配置到详细地址 | http://sa-sso-client1.com:9001/sso/login |
高 | 可以在生产环境下使用 |
疑问:为什么不直接回传 Token,而是先回传 Ticket,再用 Ticket 去查询对应的账号id?
Token 作为长时间有效的会话凭证,在任何时候都不应该直接暴露在 URL 之中(虽然 Token 直接的暴露本身不会造成安全漏洞,但会为很多漏洞提供可乘之机)
为了不让系统安全处于亚健康状态,Sa-Token-SSO 选择先回传 Ticket,再由 Ticket 获取账号id,且 Ticket 一次性用完即废,提高安全性。
七、下篇预告:跨 Redis 模式下的解决方案
以上流程解决了跨域模式下的单点登录,但是后端仍然采用了共享 Redis 来同步会话,如果我们的架构设计中Client端与Server端无法共享Redis,又该怎么完成单点登录?
这就是下篇博客要介绍的 SSO 模式三:跨域、跨 Redis 下的 SSO 对接步骤。
参考资料
- Sa-Token 文档:https://sa-token.com
- Gitee 仓库地址:https://gitee.com/dromara/sa-token
- GitHub 仓库地址:https://github.com/dromara/sa-token
鲁公网安备37011202002956号