mirror of
https://gitee.com/dromara/sa-token.git
synced 2025-09-19 10:08:07 +08:00
单点登录:模式二
This commit is contained in:
@@ -31,8 +31,9 @@
|
||||
- **进阶**
|
||||
- [全局过滤器](/use/global-filter)
|
||||
- [集群、分布式](/senior/dcs)
|
||||
- [单点登录](/senior/sso)
|
||||
<!-- - [单点登录](/senior/sso) -->
|
||||
- [多账号验证](/use/many-account)
|
||||
- [单点登录](/sso/readme)
|
||||
|
||||
- **插件**
|
||||
- [AOP注解鉴权](/plugin/aop-at)
|
||||
|
@@ -68,6 +68,7 @@
|
||||
},
|
||||
// search: 'auto', // 搜索功能
|
||||
alias: {
|
||||
'/sso/_sidebar.md': '/sso/_sidebar.md',
|
||||
'/.*/_sidebar.md': '/_sidebar.md'
|
||||
},
|
||||
// tab选项卡
|
||||
@@ -80,21 +81,23 @@
|
||||
},
|
||||
plugins: [ // 自定义插件
|
||||
function(hook, vm) {
|
||||
// 解析之后执行
|
||||
// 每次路由切换时,解析内容之后执行
|
||||
hook.afterEach(function(html) {
|
||||
var url = 'https://gitee.com/dromara/sa-token/tree/dev/sa-token-doc/doc/' + vm.route.file;
|
||||
var url2 = 'https://github.com/dromara/sa-token/tree/dev/sa-token-doc/doc/' + vm.route.file;
|
||||
var footer = [
|
||||
'<br/><br/><br/><br/><br/><br/><br/><hr/>',
|
||||
'<footer>',
|
||||
'<span>发现错误?想参与编辑? 在 <a href="' + url + '" target="_blank">Gitee</a> 或 <a href="' + url2 +
|
||||
'" target="_blank">GitHub</a> 上编辑此页!</span>',
|
||||
'<span>发现错误? 在 <a href="' + url + '" target="_blank">Gitee</a> 或 <a href="' + url2 +
|
||||
'" target="_blank">GitHub</a> 帮助我们完善此页文档!</span>',
|
||||
'<a href="https://jq.qq.com/?_wv=1027&k=45H977HM" target="_blank">点我加入QQ群</a>交流反馈',
|
||||
'</footer>'
|
||||
].join('');
|
||||
return html + footer;
|
||||
});
|
||||
// 每次路由切换时数据全部加载完成后调用,没有参数。
|
||||
hook.doneEach(function() {
|
||||
// 添加代码行数样式
|
||||
$('pre code').each(function(){
|
||||
var lines = $(this).text().split('\n').length;
|
||||
var $numbering = $('<ul/>').addClass('code-line-box');
|
||||
@@ -107,6 +110,9 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
// 初始化并第一次加载完成数据后调用,没有参数。
|
||||
hook.ready(function() {
|
||||
});
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -151,32 +157,6 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Gitalk评论 -->
|
||||
<link rel="stylesheet" href="https://unpkg.zhimg.com/gitalk@1.7.0/dist/gitalk.css">
|
||||
<script src="https://unpkg.zhimg.com/docsify@4.11.6/lib/plugins/gitalk.min.js"></script>
|
||||
<script src="https://unpkg.zhimg.com/gitalk@1.7.0/dist/gitalk.min.js"></script>
|
||||
<script>
|
||||
function f5Gitalk() {
|
||||
var id = location.hash.replace('#', '');
|
||||
if(id.indexOf('?') > -1) {
|
||||
id = id.substr(0, id.indexOf('?'));
|
||||
}
|
||||
window.gitalk = new Gitalk({
|
||||
id: id,
|
||||
clientID: '19939399448841f818a1',
|
||||
clientSecret: 'af67e0cc14a0f36e171895771c330471cfe36c23',
|
||||
repo: 'sa-token', // 仓库名称
|
||||
owner: 'dromara',
|
||||
admin: ['click33'], // 管理员列表
|
||||
// facebook-like distraction free mode
|
||||
distractionFreeMode: false
|
||||
})
|
||||
}
|
||||
f5Gitalk();
|
||||
window.onhashchange = function() {
|
||||
f5Gitalk();
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -19,6 +19,7 @@ body{font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu
|
||||
|
||||
/* 调整一下左侧树的字体样式 */
|
||||
.sidebar .sidebar-nav>ul>li>p{font-size: 1.2em; margin-top: 10px;}
|
||||
.sidebar .sidebar-nav>ul>li> strong{font-size: 1.2em; margin-top: 10px;}
|
||||
.sidebar ul li a{color: #222;}
|
||||
.sidebar .sidebar-nav>ul>li>ul>li>a{/* color: #222; */font-size: 16px; /* font-weight: 700; */}
|
||||
|
||||
@@ -26,9 +27,9 @@ body{font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu
|
||||
[alt=github-chart]{max-width: 897px;}
|
||||
/* 大屏幕时,某些图片限制一下宽度 */
|
||||
@media screen and (min-width: 800px) {
|
||||
[title=s-w]{max-width: 80%;}
|
||||
[title=s-w],[title=s-w-sh]{max-width: 80%;}
|
||||
}
|
||||
|
||||
[title=s-w-sh]{display: inline-block; border: 1px #eee solid;}
|
||||
|
||||
/* 公众号table */
|
||||
.gzh-table{ /* table-layout:fixed !important; */}
|
||||
@@ -89,3 +90,5 @@ body{font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu
|
||||
.code-line-box {padding: calc(1.5em + 1px) 0px !important; padding-bottom: calc(1.5em + 20px) !important; margin: 0px !important;}
|
||||
.code-line-box {line-height: inherit !important; background-color: #111; color: #aaa;font-weight: 400;font-size: 0.85em;text-align: center;}
|
||||
.code-line-box {font-family: source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace;}
|
||||
|
||||
|
||||
|
@@ -13,6 +13,7 @@ Sa-Token默认的Redis集成方式会把权限数据和业务缓存放在一起
|
||||
### 1、首先引入Alone-Redis依赖
|
||||
|
||||
``` xml
|
||||
<!-- Sa-Token插件:权限缓存与业务缓存分离 -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-alone-redis</artifactId>
|
||||
|
13
sa-token-doc/doc/sso/_sidebar.md
Normal file
13
sa-token-doc/doc/sso/_sidebar.md
Normal file
@@ -0,0 +1,13 @@
|
||||
<!-- 这是目录树文件 -->
|
||||
<!-- 这是目录树文件 -->
|
||||
|
||||
- **单点登录**
|
||||
- [单点登录简述](/sso/readme)
|
||||
- [SSO模式一 共享Cookie同步会话](/sso/sso-type1)
|
||||
- [SSO模式二 URL重定向传播会话](/sso/sso-type2)
|
||||
- [SSO模式三 SSO认证中心开放接口](/sso/sso-type3)
|
||||
|
||||
|
||||
|
||||
<!-- <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
|
||||
<p style="text-align: center; ">----- 到底线了 -----</p> -->
|
41
sa-token-doc/doc/sso/readme.md
Normal file
41
sa-token-doc/doc/sso/readme.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Sa-Token-SSO 单点登录模块
|
||||
|
||||
---
|
||||
|
||||
### 什么是单点登录?解决什么问题?
|
||||
举个场景:假设我们的系统被切割成N个部分:商城、论坛、直播、社交…… 如果用户每访问其中一个模块都要进行一次登录注册,那么用户将会疯掉,
|
||||
为了不让用户疯掉,我们急需一套机制将这N个系统的授权进行共享,让用户在其中一个系统登录之后,便可以畅通无阻的访问其它系统
|
||||
|
||||
单点登录——就是为了解决这个问题而生!
|
||||
|
||||
简而言之,单点登录可以做到:**`在多个系统中,用户只需登录一次,就可以访问所有系统。`**
|
||||
|
||||
|
||||
### 架构选型
|
||||
对于单点登录,网上教程很多,但大多数讲述的都是CAS重定向机制,相对来讲,CAS模式较为复杂且并不是实现单点登录的唯一方式。
|
||||
|
||||
对于不同的系统架构来讲,实现单点登录的步骤也大为不同,Sa-Token由简入难将其划分为三种模式
|
||||
|
||||
| 系统架构 | 采用模式 | 简介 | 文档链接 |
|
||||
| :-------- | :-------- | :-------- | :-------- |
|
||||
| 前端同域 + 后端同 Redis | 模式一 | 共享Cookie + 子系统 [权限缓存与业务缓存分离] | [详情](/sso/sso-type1) |
|
||||
| 前端不同域 + 后端同 Redis | 模式二 | URL重定向传播会话 | [详情]() |
|
||||
| 前端不同域 + 后端 不同Redis | 模式三 | SSO认证中心开放接口校验Ticket | [详情]() |
|
||||
|
||||
|
||||
1. 前端同域:就是指多个系统可以部署在同一个主域名之下,比如:`c1.domain.com`、`c2.domain.com`、`c3.domain.com`
|
||||
2. 后端同Redis:就是指多个系统可以连接同一个Redis,或者其它的缓存数据中心。PS:这里并不需要把所有项目的数据都放在同一个Redis中,Sa-Token提供了 **[权限缓存与业务缓存分离]** 的解决方案,详情戳:[Alone独立Redis插件](http://sa-token.dev33.cn/doc/index.html#/plugin/alone-redis)
|
||||
3. 如果既无法做到前端同域,也无法做到后端同Redis,那么只能走模式三,CAS模式(Sa-Token对CAS模式提供了完整的封装,你只需要按照示例从文档上复制几段代码便可以轻松集成)
|
||||
4. 只有根据自己的系统架构合理选择,才可对症下药,事半功倍,否则只能是劳而无功,不得要领
|
||||
|
||||
|
||||
### Sa-Token-SSO 特性
|
||||
1. API简单易用,文档介绍详细,且提供直接可用的集成示例
|
||||
2. 支持三种模式,不论是否跨域、是否共享Redis,都可以完美解决
|
||||
3. 安全性高:内置域名校验、Ticket校验、秘钥校验等,杜绝`Ticket劫持`、`Token窃取`等常见攻击手段 (文档讲述攻击原理和防御手段)
|
||||
4. 不丢参数:笔者曾试验多个单点登录框架,均有参数丢失的情况,比如重定向之前是:`http://a.com?id=1&name=2`,登录成功之后就变成了:`http://a.com?id=1`,Sa-Token-SSO内有专门的算法保证了参数不丢失,登录成功之后原路返回页面
|
||||
5. 无缝集成:由于Sa-Token本身就是一个权限认证框架,因此你可以只用一个框架同时解决`权限认证` + `单点登录`问题,让你不再到处搜索:xxx单点登录与xxx权限认证如何整合……
|
||||
6. 高可定制:Sa-Token-SSO模块对代码架构侵入性极低,结合Sa-Token本身的路由拦截特性,你可以非常轻松的定制化开发
|
||||
|
||||
|
||||
|
171
sa-token-doc/doc/sso/sso-type1.md
Normal file
171
sa-token-doc/doc/sso/sso-type1.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# SSO模式一 共享Cookie同步会话
|
||||
|
||||
如果我们的系统可以保证部署在同一个主域名之下,并且后端连接同一个Redis,那么便可以使用 `[共享Cookie同步会话]` 的方式做到单点登录
|
||||
|
||||
---
|
||||
|
||||
### 解决思路?
|
||||
|
||||
首先我们分析一下多个系统之间为什么无法同步登录状态?
|
||||
1. 前端的`Token`无法在多个系统下共享
|
||||
2. 后端的`Session`无法在多个系统间共享
|
||||
|
||||
所以单点登录第一招,就是对症下药:
|
||||
1. 使用`共享Cookie`来解决Token共享问题
|
||||
2. 使用`Redis`来解决Session共享问题
|
||||
|
||||
所谓共享Cookie,就是主域名Cookie在二级域名下的共享,举个例子:写在父域名`stp.com`下的Cookie,在`s1.stp.com`、`s2.stp.com`等子域名都是可以共享访问的
|
||||
|
||||
而共享Redis,并不需要我们把所有项目的数据都放在同一个Redis中,Sa-Token提供了 **[权限缓存与业务缓存分离]** 的解决方案,详情戳:[Alone独立Redis插件](/plugin/alone-redis)
|
||||
|
||||
> PS:这里建议不要用B项目去连接A项目的Redis,也不要A项目连接B项目的Redis,而是抽离出一个单独的 SSO-Redis,A项目和B项目一起来连接这个 SSO-Redis
|
||||
|
||||
OK,所有理论就绪,下面开始实战
|
||||
|
||||
|
||||
### 集成步骤
|
||||
|
||||
Sa-Token整合同域下的单点登录非常简单,相比于正常的登录,你只需要在配置文件中增加配置 `sa-token.cookie-domain=xxx.com` 来指定一下Cookie写入时指定的父级域名即可,详细步骤示例如下:
|
||||
|
||||
#### 1. 准备工作
|
||||
首先修改hosts文件`(C:\WINDOWS\system32\drivers\etc\hosts)`,添加以下IP映射,方便我们进行测试:
|
||||
``` text
|
||||
127.0.0.1 s1.stp.com
|
||||
127.0.0.1 s2.stp.com
|
||||
127.0.0.1 s3.stp.com
|
||||
```
|
||||
|
||||
#### 2. 指定Cookie的作用域
|
||||
常规情况下,在`s1.stp.com`域名访问服务器,其Cookie也只能写入到`s1.stp.com`下,为了将Cookie写入到其父级域名`stp.com`下,我们需要在配置文件中新增配置:
|
||||
``` yml
|
||||
spring:
|
||||
sa-token:
|
||||
# 写入Cookie时显式指定的作用域, 用于单点登录二级域名共享Cookie
|
||||
cookie-domain: stp.com
|
||||
```
|
||||
|
||||
#### 3. 新增测试Controller
|
||||
新建`SSOController.java`控制器,写入代码:
|
||||
``` java
|
||||
/**
|
||||
* 测试: 同域单点登录
|
||||
* @author kong
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/sso/")
|
||||
public class SSOController {
|
||||
|
||||
// 测试:进行登录
|
||||
@RequestMapping("doLogin")
|
||||
public AjaxJson doLogin(@RequestParam(defaultValue = "10001") String id) {
|
||||
System.out.println("---------------- 进行登录 ");
|
||||
StpUtil.login(id);
|
||||
return AjaxJson.getSuccess("登录成功: " + id);
|
||||
}
|
||||
|
||||
// 测试:是否登录
|
||||
@RequestMapping("isLogin")
|
||||
public AjaxJson isLogin() {
|
||||
System.out.println("---------------- 是否登录 ");
|
||||
boolean isLogin = StpUtil.isLogin();
|
||||
return AjaxJson.getSuccess("是否登录: " + isLogin);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
#### 4、访问测试
|
||||
启动项目,依次访问:
|
||||
- [http://s1.stp.com:8081/sso/isLogin](http://s1.stp.com:8081/sso/isLogin)
|
||||
- [http://s2.stp.com:8081/sso/isLogin](http://s2.stp.com:8081/sso/isLogin)
|
||||
- [http://s3.stp.com:8081/sso/isLogin](http://s3.stp.com:8081/sso/isLogin)
|
||||
|
||||
均返回以下结果:
|
||||
``` js
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "是否登录: false",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
现在访问任意节点的登录接口:
|
||||
- [http://s1.stp.com:8081/sso/doLogin](http://s1.stp.com:8081/sso/doLogin)
|
||||
|
||||
``` js
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "登录成功: 10001",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
然后再次刷新上面三个测试接口,均可以得到以下结果:
|
||||
``` js
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "是否登录: true",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
测试完毕
|
||||
|
||||
|
||||
### 跨域模式下的解决方案
|
||||
|
||||
如上,我们使用极其简单的步骤实现了同域下的单点登录,聪明如你😏,马上想到了这种模式有着一个不小的限制:
|
||||
|
||||
> 所有子系统的域名,必须同属一个父级域名
|
||||
|
||||
如果我们的子系统在完全不同的域名下,我们又该怎么完成单点登录功能呢?
|
||||
|
||||
且往下看,[SSO模式二:URL重定向传播会话](/sso/sso-type2)
|
||||
|
||||
<!-- 根据前面的总结,单点登录的关键点在于我们如何完成多个系统之间的token共享,而`Cookie`并非实现此功能的唯一方案,既然浏览器对`Cookie`限制重重,我们何不干脆直接放弃`Cookie`,转投`LocalStorage`的怀抱?
|
||||
|
||||
思路:建立一个登录中心,在中心登录之后将token一次性下发到所有子系统中
|
||||
|
||||
参考以下步骤:
|
||||
``` js
|
||||
// 在主域名登录请求回调函数里执行以下方法
|
||||
|
||||
// 获取token
|
||||
var token = res.data.tokenValue;
|
||||
|
||||
// 创建子域的iframe, 用于传送数据
|
||||
var iframe = document.createElement("iframe");
|
||||
iframe.src = "http://s2.stp.com/xxx.html";
|
||||
iframe.style.display = 'none';
|
||||
document.body.append(iframe);
|
||||
|
||||
// 使用postMessage()发送数据到子系统
|
||||
setTimeout(function () {
|
||||
iframe.contentWindow.postMessage(token, "http://s2.stp.com");
|
||||
}, 2000);
|
||||
|
||||
// 销毁iframe
|
||||
setTimeout(function () {
|
||||
iframe.remove();
|
||||
}, 4000);
|
||||
|
||||
|
||||
// 在子系统里接受消息
|
||||
window.addEventListener('message', function (event) {
|
||||
console.log('收到消息', event.data);
|
||||
// 写入本地localStorage缓存中
|
||||
localStorage.setItem('satoken', event.data)
|
||||
}, false);
|
||||
|
||||
```
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
总结:此方式仍然限制较大,但巧在提供了一种简便的思路做到了跨域共享token,其实跨域模式下的单点登录标准解法还是cas流程,
|
||||
参考[单点登录的三种方式](https://www.cnblogs.com/yonghengzh/p/13712729.html) -->
|
||||
|
||||
|
||||
|
||||
|
||||
|
397
sa-token-doc/doc/sso/sso-type2.md
Normal file
397
sa-token-doc/doc/sso/sso-type2.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# SSO模式二 URL重定向传播会话
|
||||
|
||||
如果我们的系统部署在不同的域名之下,但是后端可以连接同一个Redis,那么便可以使用 [URL重定向传播会话] 的方式做到单点登录
|
||||
|
||||
|
||||
### 0、解题思路
|
||||
|
||||
首先我们再次复习一下多个系统之间为什么无法同步登录状态?
|
||||
|
||||
1. 前端的`Token`无法在多个系统下共享
|
||||
2. 后端的`Session`无法在多个系统间共享
|
||||
|
||||
关于第二点,我们已经在"SSO模式一"章节中阐述,使用 [Alone独立Redis插件](/plugin/alone-redis) 做到权限缓存直连SSO-Redis数据中心,在此不再赘述
|
||||
|
||||
而第一点,才是我们解决问题的关键所在,在跨域模式下,意味着"共享Cookie方案"的失效,我们必须采用一种新的方案来传递Token
|
||||
|
||||
1. 用户在 子系统 点击`[登录]`按钮
|
||||
2. 用户跳转到子系统登录页面,并携带`back参数`记录初始页面URL
|
||||
3. 子系统检测到此用户尚未登录,再次将其重定向至SSO认证中心,并携带`redirect参数`记录子系统的登录页URL
|
||||
4. 用户在SSO认证中心尚未登录,开始登录
|
||||
5. 用户在SSO认证中心登录成功,重定向至子系统的登录页URL,并携带`ticket码`
|
||||
6. 子系统使用ticket码从`SSO-Redis`中获取账号id,并在子系统登录此账号会话
|
||||
7. 子系统将用户再次重定向至最初始的`back`页面
|
||||
|
||||
整个过程,除了第四步用户在SSO认证中心登录时会被打断,其余过程均是自动化的,当用户在另一个子系统再次点击`[登录]`按钮,由于此用户在SSO认证中心已有会话登录,
|
||||
所以第四步也将自动化,也就是单点登录的最终目的 —— 一次登录,处处通行。
|
||||
|
||||
下面我们按照步骤依次完成上述步骤
|
||||
|
||||
|
||||
### 1、搭建SSO-Server认证中心
|
||||
|
||||
> 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso-server/`,如遇到难点可结合源码进行测试学习
|
||||
|
||||
##### 1.1、创建SSO-Server端项目
|
||||
创建一个SpringBoot项目 `sa-token-demo-sso-server`(不会的同学自行百度或参考仓库示例),添加pom依赖:
|
||||
|
||||
``` xml
|
||||
<!-- SpringBoot依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot-starter</artifactId>
|
||||
<version>1.20.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token整合redis (使用jackson序列化方式) -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-dao-redis-jackson</artifactId>
|
||||
<version>1.20.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 提供Redis连接池 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
##### 1.2、创建SSO-Server端认证接口
|
||||
``` java
|
||||
/**
|
||||
* Sa-Token-SSO Server端 Controller
|
||||
*/
|
||||
@RestController
|
||||
public class SsoServerController {
|
||||
|
||||
// SSO-Server端:授权地址,跳转到登录页面
|
||||
@RequestMapping("ssoAuth")
|
||||
public Object ssoAuth(String redirect) {
|
||||
/*
|
||||
* 此处两种情况分开处理:
|
||||
* 1、如果在SSO认证中心尚未登录,则先去登登录
|
||||
* 2、如果在SSO认证中心尚已登录,则开始对redirect地址下放ticket引导授权
|
||||
*/
|
||||
// 情况1:尚未登录
|
||||
if(StpUtil.isLogin() == false) {
|
||||
String msg = "当前会话在SSO-Server端尚未登录,请先访问"
|
||||
+ "<a href='/doLogin?name=sa&pwd=123456' target='_blank'> doLogin登录 </a>"
|
||||
+ "进行登录之后,刷新页面开始授权";
|
||||
return msg;
|
||||
}
|
||||
// 情况2:已经登录,开始构建授权重定向地址,下放ticket
|
||||
String redirectUrl = SaSsoUtil.buildRedirectUrl(StpUtil.getLoginId(), redirect);
|
||||
return new ModelAndView("redirect:" + redirectUrl);
|
||||
}
|
||||
|
||||
// SSO-Server端:登录接口
|
||||
@RequestMapping("doLogin")
|
||||
public AjaxJson doLogin(String name, String pwd) {
|
||||
// 此处仅做模拟登录,真实环境应该查询数据进行登录
|
||||
if("sa".equals(name) && "123456".equals(pwd)) {
|
||||
StpUtil.login(10001);
|
||||
return AjaxJson.getSuccess("登录成功!");
|
||||
}
|
||||
return AjaxJson.getError("登录失败!");
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
##### 1.4、application.yml配置
|
||||
``` yml
|
||||
# 端口
|
||||
server:
|
||||
port: 9000
|
||||
|
||||
spring:
|
||||
# Sa-Token配置
|
||||
sa-token:
|
||||
# SSO-相关配置
|
||||
sso:
|
||||
# Ticket有效期 (单位: 秒),默认三分钟
|
||||
ticket-timeout: 300
|
||||
# 所有允许的授权回调地址 (此处为了方便测试配置为*,线上生产环境一定要配置为详细地地址)
|
||||
allow-url: "*"
|
||||
# 接口调用秘钥(模式三才会用到此参数)
|
||||
# secret-key:
|
||||
|
||||
# Redis配置
|
||||
redis:
|
||||
# Redis数据库索引(默认为0)
|
||||
database: 1
|
||||
# Redis服务器地址
|
||||
host: 127.0.0.1
|
||||
# Redis服务器连接端口
|
||||
port: 6379
|
||||
# Redis服务器连接密码(默认为空)
|
||||
password:
|
||||
```
|
||||
注意点:`allow-url`为了方便测试配置为*,线上生产环境一定要配置为详细URL地址
|
||||
|
||||
##### 1.4、创建SSO-Server端启动类
|
||||
``` java
|
||||
@SpringBootApplication
|
||||
public class SaSsoServerApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SaSsoServerApplication.class, args);
|
||||
System.out.println("\nSa-Token-SSO 认证中心启动成功");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
启动此项目,用作**`SSO-Server认证中心`**
|
||||
|
||||
|
||||
### 2、搭建SSO-Client应用端
|
||||
|
||||
> 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso-client/`,如遇到难点可结合源码进行测试学习
|
||||
|
||||
##### 2.1、创建SSO-Client端项目
|
||||
创建一个SpringBoot项目 `sa-token-demo-sso-client`,添加pom依赖:
|
||||
``` xml
|
||||
<!-- SpringBoot依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot-starter</artifactId>
|
||||
<version>${sa-token-version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token 整合redis (使用jackson序列化方式) -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-dao-redis-jackson</artifactId>
|
||||
<version>${sa-token-version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 提供Redis连接池 -->
|
||||
<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.20.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
|
||||
##### 1.2、创建SSO-Client端认证接口
|
||||
``` java
|
||||
|
||||
/**
|
||||
* Sa-Token-SSO Client端 Controller
|
||||
*/
|
||||
@RestController
|
||||
public class SsoClientController {
|
||||
|
||||
// SSO-Client端:首页
|
||||
@RequestMapping("/")
|
||||
public String index() {
|
||||
String str = "<h2>Sa-Token SSO-Client 应用端</h2>" +
|
||||
"<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
|
||||
"<p><a href=\"javascript:location.href='/ssoLogin?back=' + lencodeURIComponent(location.href);\">登录</a></p>";
|
||||
return str;
|
||||
}
|
||||
|
||||
// SSO-Client端:登录地址
|
||||
@RequestMapping("ssoLogin")
|
||||
public Object login(String back, String ticket) {
|
||||
// 如果当前Client端已经登录,则无需访问SSO认证中心,可以直接返回
|
||||
if(StpUtil.isLogin()) {
|
||||
return new ModelAndView("redirect:" + back);
|
||||
}
|
||||
/*
|
||||
* 接下来两种情况:
|
||||
* ticket有值,说明此请求从SSO认证中心重定向而来,需要根据ticket进行登录
|
||||
* ticket无值,说明此请求是Client端访问,需要重定向至SSO认证中心
|
||||
*/
|
||||
if(ticket != null) {
|
||||
Object loginId = SaSsoUtil.getLoginId(ticket);
|
||||
if(loginId != null ) {
|
||||
// 如果ticket是有效的 (可以获取到值),需要就此登录 且清除此ticket
|
||||
StpUtil.login(loginId);
|
||||
SaSsoUtil.deleteTicket(ticket);
|
||||
// 最后重定向回back地址
|
||||
return new ModelAndView("redirect:" + back);
|
||||
}
|
||||
// 此处向客户端提示ticket无效即可,不要重定向到SSO认证中心,否则容易引起无限重定向
|
||||
return "ticket无效: " + ticket;
|
||||
}
|
||||
|
||||
// 重定向至 SSO-Server端 认证地址
|
||||
String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(SaHolder.getRequest().getUrl(), back);
|
||||
return new ModelAndView("redirect:" + serverAuthUrl);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
##### 1.3、配置SSO认证中心地址
|
||||
你需要在 `application.yml` 配置如下信息:
|
||||
``` yml
|
||||
# 端口
|
||||
server:
|
||||
port: 9001
|
||||
|
||||
spring:
|
||||
# sa-token配置
|
||||
sa-token:
|
||||
# SSO-相关配置
|
||||
sso:
|
||||
# SSO-Server端授权地址
|
||||
server-url: http://sa-sso-server.com:9000/ssoAuth
|
||||
|
||||
# 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis)
|
||||
alone-redis:
|
||||
# Redis数据库索引 (默认为0)
|
||||
database: 1
|
||||
# Redis服务器地址
|
||||
host: 127.0.0.1
|
||||
# Redis服务器连接端口
|
||||
port: 6379
|
||||
# Redis服务器连接密码(默认为空)
|
||||
password:
|
||||
```
|
||||
注意点:`spring.sa-token.alone-redis` 的配置需要和SSO-Server端连接同一个Redis(database也要一样)
|
||||
|
||||
##### 1.4、写启动类
|
||||
``` java
|
||||
@SpringBootApplication
|
||||
public class SaSsoClientApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SaSsoClientApplication.class, args);
|
||||
System.out.println("\nSa-Token-SSO Client端启动成功");
|
||||
}
|
||||
}
|
||||
```
|
||||
启动项目
|
||||
|
||||
|
||||
### 3、测试访问
|
||||
|
||||
##### 3.1 修改host文件
|
||||
首先修改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
|
||||
```
|
||||
|
||||
##### 3.2 启动项目并访问
|
||||
(1) 依次启动SSO-Server与SSO-Client端,然后从浏览器访问:[http://sa-sso-client1.com:9001/](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/](http://sa-sso-client2.com:9001/)
|
||||
|
||||

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

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

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

|
||||
|
||||
|
||||
### 4、运行官方仓库
|
||||
|
||||
以上示例,虽然完整的复现了单点登录的过程,但是页面还是有些简陋,我们可以运行一下官方仓库的示例,里面有制作好的登录页面
|
||||
|
||||
> 下载官方示例,依次运行 `/sa-token-demo/sa-token-demo-sso-client/` 和 `/sa-token-demo/sa-token-demo-sso-server/`,访问:[http://sa-sso-client1.com:9001/](http://sa-sso-client1.com:9001/)
|
||||
|
||||

|
||||
|
||||
默认测试密码:`sa / 123456`,其余流程保持不变
|
||||
|
||||
|
||||
### 5、配置域名校验
|
||||
|
||||
##### 5.1、Ticket劫持攻击
|
||||
在以上的SSO-Server端示例中,配置项 `spring.sa-token.sso.allow-url=*` 意为配置所有允许的Client端授权地址,不在此配置项中的URL将无法单点登录成功
|
||||
|
||||
以上示例为了方便测试被配置为*,但是,<font color="#FF0000" >在生产环境中,此配置项绝对不能配置为 * </font>,否则会有被ticket劫持的风险
|
||||
|
||||
假设攻击者根据模仿我们的授权地址,巧妙的构造一个URL
|
||||
|
||||
> [http://sa-sso-server.com:9000/ssoAuth?redirect=https://www.baidu.com/](http://sa-sso-server.com:9000/ssoAuth?redirect=https://www.baidu.com/)
|
||||
|
||||
当不知情的小红被诱导访问了这个URL时,它将被重定向至百度首页
|
||||
|
||||

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

|
||||
|
||||
域名没有通过校验,拒绝授权!
|
||||
|
||||
##### 5.3、配置安全性参考表
|
||||
|
||||
| 配置方式 | 举例 | 安全性 | 建议 |
|
||||
| :-------- | :-------- | :-------- | :-------- |
|
||||
| 配置为* | `*` | <font color="#F00" >低</font> | <font color="#F00" >禁止在生产环境下使用</font> |
|
||||
| 配置到域名 | `http://sa-sso-client1.com/*` | <font color="#F70" >中</font> | <font color="#F70" >不建议在生产环境下使用</font> |
|
||||
| 配置到详细地址| `http://sa-sso-client1.com:9001/ssoLogin` | <font color="#080" >高</font> | <font color="#080" >可以在生产环境下使用</font> |
|
||||
|
||||
|
||||
##### 5.4、疑问:为什么不直接回传Token,而是先回传ticket,再用ticket去查询对应的账号id?
|
||||
Token作为长时间有效的会话凭证,在任何时候都不应该直接在暴露URL之中(虽然Token直接的暴露本身不会造成安全漏洞,但会为很多漏洞提供可乘之机)
|
||||
|
||||
因此Sa-Token-SSO选择先回传ticket,再由ticket获取账号id,且ticket一次性用完即废,提高安全性
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
2
sa-token-doc/doc/sso/sso-type3.md
Normal file
2
sa-token-doc/doc/sso/sso-type3.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# SSO模式三 SSO认证中心开放接口校验Ticket
|
||||
|
Reference in New Issue
Block a user