Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84d52f1631 | |||
| 1603872c83 | |||
| c63c9d6e20 | |||
| 35deae30f5 | |||
| 1cea62ff22 | |||
| ad79e3595e | |||
| 2401726e01 | |||
| c129be45ed | |||
| be6cd36640 | |||
| f745dcefd2 | |||
| 949da31ae0 | |||
| f9c4aa1549 | |||
| 3eb294668e | |||
| cc4cc89e27 | |||
| 3567e9f54b | |||
| 84679d5fd4 | |||
| adbc1003b4 | |||
| 35cf52c684 | |||
| 38903d5a46 | |||
| 9530e63d3b | |||
| be51ff27a1 | |||
| bcc1e0cc93 | |||
| 5294b23652 | |||
| 142f2994cb | |||
| 4e7ab59dcf | |||
| ef4514d873 | |||
| efc41e1504 | |||
| 66639eda83 | |||
| 798a5548f9 | |||
| 1644a1c5f3 | |||
| d8ad47193e | |||
| 35814bbb66 | |||
| ef1bdbd867 | |||
| 4d10dd0d48 | |||
| d6b5615635 | |||
| 5855e71413 | |||
| 4eaddefc21 | |||
| fbe63e591e | |||
| 0db3312a63 | |||
| 2aad6d4413 | |||
| 8651ddad1e | |||
| 952fa04658 | |||
| 1f896ef584 | |||
| 03f66c6081 | |||
| 6a3b058dd3 | |||
| c1ab2cdd82 | |||
| a88c8058fd | |||
| 02f082b400 | |||
| 3372cf190e | |||
| d4493d0f98 | |||
| 5d5e2a5d52 | |||
| 64beb7a18a | |||
| bffdd0f2e1 | |||
| fef0d8f3e2 | |||
| bc8339e13d | |||
| bea2592dc9 | |||
| cf93324053 | |||
| cbb7713c9f | |||
| 010f4d3c88 | |||
| 2842a7a56e | |||
| ef1507e5b7 | |||
| 95beaee6ee | |||
| cbc28d392b | |||
| 23a9fb3447 | |||
| 93714d28e0 | |||
| b5f23f2455 | |||
| c7ca8ee280 | |||
| 34d1008499 | |||
| de7ccf05aa | |||
| 6d44299902 | |||
| ee80633582 | |||
| 971c2860f0 | |||
| 742b65366a | |||
| 93e231ff18 | |||
| ac35d77f3b | |||
| 9f097b9c01 | |||
| 9ddc85d876 | |||
| 3021f2c871 | |||
| ffb002dbb6 | |||
| 879894e5a7 | |||
| 4e935cb054 | |||
| 0290c010b1 | |||
| 20ec0857cc | |||
| 0bc8982212 | |||
| 936dfe333d | |||
| 0a5c5da4b4 | |||
| 03d0f235d4 | |||
| d333b07c58 | |||
| 922e746eb1 | |||
| 82f7d7f78c | |||
| 12d76e34bb |
@@ -12,3 +12,5 @@ unpackage/
|
||||
/.factorypath
|
||||
|
||||
.idea/
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<p align="center">
|
||||
<img alt="logo" src="https://gitee.com/dromara/sa-token/raw/master/sa-token-doc/doc/logo.png" width="150" height="150">
|
||||
</p>
|
||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Sa-Token v1.21.0</h1>
|
||||
<h4 align="center">这可能是史上功能最全的 Java 权限认证框架!</h4>
|
||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Sa-Token v1.26.0</h1>
|
||||
<h4 align="center">一个轻量级 Java 权限认证框架,让鉴权变得简单、优雅!</h4>
|
||||
<p align="center">
|
||||
<a href="https://gitee.com/dromara/sa-token/stargazers"><img src="https://gitee.com/dromara/sa-token/badge/star.svg"></a>
|
||||
<a href="https://gitee.com/dromara/sa-token/members"><img src="https://gitee.com/dromara/sa-token/badge/fork.svg"></a>
|
||||
@@ -15,33 +15,88 @@
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 在线资料
|
||||
|
||||
## 前言:
|
||||
- [在线文档:http://sa-token.dev33.cn/](http://sa-token.dev33.cn/)
|
||||
|
||||
- [需求提交:我们深知一个优秀的项目需要海纳百川,点我在线提交需求](http://sa-app.dev33.cn/wall.html?name=sa-token)
|
||||
- 我们将会尽力讲解每个功能的设计原因、应用场景,用心阅读文档,你学习到的将不止是 `Sa-Token` 框架本身,更是绝大多数场景下权限设计的最佳实践。
|
||||
|
||||
- [开源不易,求鼓励,点个star吧 !](###)
|
||||
## Sa-Token 介绍
|
||||
|
||||
**Sa-Token** 是一个轻量级 Java 权限认证框架,主要解决:**`登录认证`**、**`权限认证`**、**`Session会话`**、**`单点登录`**、**`OAuth2.0`**、**`微服务网关鉴权`**
|
||||
等一系列权限相关问题。
|
||||
|
||||
Sa-Token 的 API 设计非常简单,有多简单呢?以登录认证为例,你只需要:
|
||||
|
||||
``` java
|
||||
// 在登录时写入当前会话的账号id
|
||||
StpUtil.login(10001);
|
||||
|
||||
// 然后在需要校验登录处调用以下方法:
|
||||
// 如果当前会话未登录,这句代码会抛出 `NotLoginException` 异常
|
||||
StpUtil.checkLogin();
|
||||
```
|
||||
|
||||
至此,我们已经借助 Sa-Token 完成登录认证!
|
||||
|
||||
此时的你小脑袋可能飘满了问号,就这么简单?自定义 Realm 呢?全局过滤器呢?我不用写各种配置文件吗?
|
||||
|
||||
没错,在 Sa-Token 中,登录认证就是如此简单,不需要任何的复杂前置工作,只需这一行简单的API调用,就可以完成会话登录认证!
|
||||
|
||||
当你受够 Shiro、SpringSecurity 等框架的三拜九叩之后,你就会明白,相对于这些传统老牌框架,Sa-Token 的 API 设计是多么的简单、优雅!
|
||||
|
||||
权限认证示例(只有具备 `user:add` 权限的会话才可以进入请求)
|
||||
``` java
|
||||
@SaCheckPermission("user:add")
|
||||
@RequestMapping("/user/insert")
|
||||
public String insert(SysUser user) {
|
||||
// ...
|
||||
return "用户增加";
|
||||
}
|
||||
```
|
||||
|
||||
将某个账号踢下线(待到对方再次访问系统时会抛出`NotLoginException`异常)
|
||||
``` java
|
||||
// 使账号id为 10001 的会话强制注销登录
|
||||
StpUtil.logoutByLoginId(10001);
|
||||
```
|
||||
|
||||
在 Sa-Token 中,绝大多数功能都可以 **一行代码** 完成:
|
||||
``` java
|
||||
StpUtil.login(10001); // 标记当前会话登录的账号id
|
||||
StpUtil.getLoginId(); // 获取当前会话登录的账号id
|
||||
StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false
|
||||
StpUtil.logout(); // 当前会话注销登录
|
||||
StpUtil.logoutByLoginId(10001); // 让账号为10001的会话注销登录(踢人下线)
|
||||
StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false
|
||||
StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false
|
||||
StpUtil.getSession(); // 获取当前账号id的Session
|
||||
StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
|
||||
StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值
|
||||
StpUtil.login(10001, "PC"); // 指定设备标识登录,常用于“同端互斥登录”
|
||||
StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响)
|
||||
StpUtil.openSafe(120); // 在当前会话开启二级认证,有效期为120秒
|
||||
StpUtil.checkSafe(); // 校验当前会话是否处于二级认证有效期内,校验失败会抛出异常
|
||||
StpUtil.switchTo(10044); // 将当前会话身份临时切换为其它账号
|
||||
```
|
||||
|
||||
即使不运行测试,相信您也能意会到绝大多数 API 的用法。
|
||||
|
||||
|
||||
## Sa-Token 是什么?
|
||||
Sa-Token是一个轻量级Java权限认证框架,主要解决:登录认证、权限认证、Session会话、单点登录、OAuth2.0 等一系列权限相关问题
|
||||
|
||||
框架集成简单、开箱即用、API设计清爽,通过Sa-Token,你将以一种极其简单的方式实现系统的权限认证部分
|
||||
## Sa-Token 功能一览
|
||||
|
||||
## Sa-Token 能做什么?
|
||||
|
||||
- **登录验证** —— 单端登录、多端登录、同端互斥登录、七天内免登录
|
||||
- **权限验证** —— 权限认证、角色认证、会话二级认证
|
||||
- **登录认证** —— 单端登录、多端登录、同端互斥登录、七天内免登录
|
||||
- **权限认证** —— 权限认证、角色认证、会话二级认证
|
||||
- **Session会话** —— 全端共享Session、单端独享Session、自定义Session
|
||||
- **踢人下线** —— 根据账号id踢人下线、根据Token值踢人下线
|
||||
- **账号封禁** —— 指定天数封禁、永久封禁、设定解封时间
|
||||
- **持久层扩展** —— 可集成Redis、Memcached等专业缓存中间件,重启数据不丢失
|
||||
- **分布式会话** —— 提供jwt集成、共享数据中心两种分布式会话方案
|
||||
- **微服务网关鉴权** —— 适配Gateway、Soul、Zuul等常见网关的路由拦截认证
|
||||
- **微服务网关鉴权** —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证
|
||||
- **单点登录** —— 内置三种单点登录模式:无论是否跨域、是否共享Redis,都可以搞定
|
||||
- **OAuth2.0认证** —— 基于RFC-6749标准编写,OAuth2.0标准流程的授权认证,支持openid模式
|
||||
- **二级认证** —— 在已登录的基础上再次认证,保证安全性
|
||||
- **Basic认证** —— 一行代码接入 Http Basic 认证
|
||||
- **独立Redis** —— 将权限缓存与业务缓存分离
|
||||
- **临时Token验证** —— 解决短时间的Token授权问题
|
||||
- **模拟他人账号** —— 实时操作任意用户状态数据
|
||||
@@ -60,12 +115,6 @@ Sa-Token是一个轻量级Java权限认证框架,主要解决:登录认证
|
||||
- **开箱即用** —— 提供SpringMVC、WebFlux等常见web框架starter集成包,真正的开箱即用
|
||||
- **更多功能正在集成中...** —— 如有您有好想法或者建议,欢迎加群交流
|
||||
|
||||
##### Sa-Token 功能结构图
|
||||

|
||||
|
||||
##### Sa-Token 认证流程图
|
||||

|
||||
|
||||
|
||||
## Sa-Token-SSO 单点登录
|
||||
对于单点登录,网上教程大多以CAS模式为主,其实对于不同的系统架构,实现单点登录的步骤也大为不同,Sa-Token由简入难将其划分为三种模式:
|
||||
@@ -85,67 +134,29 @@ Sa-Token是一个轻量级Java权限认证框架,主要解决:登录认证
|
||||
|
||||
## Sa-Token-SSO 特性
|
||||
1. API简单易用,文档介绍详细,且提供直接可用的集成示例
|
||||
2. 支持三种模式,不论是否跨域、是否共享Redis,都可以完美解决
|
||||
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本身的路由拦截特性,你可以非常轻松的定制化开发
|
||||
|
||||
|
||||
## Sa-Token-OAuth2.0 授权登录
|
||||
Sa-OAuth2 模块基于 [RFC-6749 标准](https://tools.ietf.org/html/rfc6749) 编写,通过Sa-OAuth2你可以非常轻松的实现系统的OAuth2.0授权认证
|
||||
|
||||
## 代码示例
|
||||
1. 授权码(Authorization Code):OAuth2.0标准授权步骤,Server端向Client端下放Code码,Client端再用Code码换取授权Token
|
||||
2. 隐藏式(Implicit):无法使用授权码模式时的备用选择,Server端使用URL重定向方式直接将Token下放到Client端页面
|
||||
3. 密码式(Password):Client直接拿着用户的账号密码换取授权Token
|
||||
4. 客户端凭证(Client Credentials):Server端针对Client级别的Token,代表应用自身的资源授权
|
||||
|
||||
Sa-Token的API调用非常简单,有多简单呢?以登录验证为例,你只需要:
|
||||
详细参考文档:[http://sa-token.dev33.cn/doc/index.html#/oauth2/readme](http://sa-token.dev33.cn/doc/index.html#/oauth2/readme)
|
||||
|
||||
``` java
|
||||
// 在登录时写入当前会话的账号id
|
||||
StpUtil.login(10001);
|
||||
|
||||
// 然后在任意需要校验登录处调用以下API
|
||||
// 如果当前会话未登录,这句代码会抛出 `NotLoginException`异常
|
||||
StpUtil.checkLogin();
|
||||
```
|
||||
至此,我们已经借助Sa-Token框架完成登录授权!
|
||||
|
||||
此时的你小脑袋可能飘满了问号,就这么简单?自定义Realm呢?全局过滤器呢?我不用写各种配置文件吗?
|
||||
## Sa-Token 功能结构图
|
||||

|
||||
|
||||
事实上在此我可以负责的告诉你,在Sa-Token中,登录授权就是如此的简单,不需要什么全局过滤器,不需要各种乱七八糟的配置!只需要这一行简单的API调用,即可完成会话的登录授权!
|
||||
|
||||
当你受够Shiro、Spring Security等框架的三拜九叩之后,你就会明白,相对于这些传统老牌框架,Sa-Token的API设计是多么的清爽!
|
||||
|
||||
权限认证示例 (只有具有`user:add`权限的会话才可以进入请求)
|
||||
``` java
|
||||
@SaCheckPermission("user:add")
|
||||
@RequestMapping("/user/insert")
|
||||
public String insert(SysUser user) {
|
||||
// ...
|
||||
return "用户增加";
|
||||
}
|
||||
```
|
||||
|
||||
将某个账号踢下线 (待到对方再次访问系统时会抛出`NotLoginException`异常)
|
||||
``` java
|
||||
// 使账号id为10001的会话注销登录
|
||||
StpUtil.logoutByLoginId(10001);
|
||||
```
|
||||
|
||||
除了以上的示例,Sa-Token还可以一行代码完成以下功能:
|
||||
``` java
|
||||
StpUtil.login(10001); // 标记当前会话登录的账号id
|
||||
StpUtil.getLoginId(); // 获取当前会话登录的账号id
|
||||
StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false
|
||||
StpUtil.logout(); // 当前会话注销登录
|
||||
StpUtil.logoutByLoginId(10001); // 让账号为10001的会话注销登录(踢人下线)
|
||||
StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false
|
||||
StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false
|
||||
StpUtil.getSession(); // 获取当前账号id的Session
|
||||
StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
|
||||
StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值
|
||||
StpUtil.login(10001, "PC"); // 指定设备标识登录
|
||||
StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响)
|
||||
StpUtil.switchTo(10044); // 将当前会话身份临时切换为其它账号
|
||||
```
|
||||
Sa-Token API 众多,请恕此处无法为您逐一展示,更多示例请戳官方在线文档
|
||||

|
||||
|
||||
|
||||
## Star 趋势
|
||||
@@ -154,34 +165,30 @@ Sa-Token API 众多,请恕此处无法为您逐一展示,更多示例请戳
|
||||
[](https://starchart.cc/dromara/sa-token)
|
||||
|
||||
|
||||
## 参与贡献
|
||||
众人拾柴火焰高,万丈高楼众人起!
|
||||
Sa-Token秉承着开放的思想,欢迎大家为框架添砖加瓦:
|
||||
## 使用Sa-Token的开源项目
|
||||
- **[ sa-plus ]**:[一个基于 SpringBoot 架构的快速开发框架,内置代码生成器](https://gitee.com/click33/sa-plus)
|
||||
|
||||
1. 核心代码:该部分需要开发者了解整个框架的架构,遵循已有代码规范进行bug修复或提交新功能
|
||||
2. 文档部分:需要以清晰明了的语句书写文档,力求简单易读,授人以鱼同时更授人以渔
|
||||
3. 社区建设:如果框架帮助到了您,希望您可以加入qq群参与交流,对不熟悉框架的新人进行排难解惑
|
||||
4. 框架推广:一个优秀的开源项目不能仅靠闭门造车,它还需要一定的推广方案让更多的人一起参与到项目中
|
||||
5. 其它部分:您可以参考项目issues与需求墙进行贡献
|
||||
- **[ jthink ]**: [一个基于 SpringBoot + Sa-Token + Thymeleaf 的博客系统](https://gitee.com/wtsoftware/jthink)
|
||||
|
||||
作者寄语:参与贡献不光只有提交代码,点一个star、提一个issues都是对开源项目的促进,
|
||||
如果Sa-Token帮助到了你,欢迎你把框架推荐给朋友、同事使用,为Sa-Token的推广做一份贡献
|
||||
- **[ dcy-fast ]**:[ 一个基于 SpringBoot + Sa-Token + Mybatis-Plus 的后台管理系统,前端vue-element-admin,并且内置代码生成器](https://gitee.com/dcy421/dcy-fast)
|
||||
|
||||
- **[ helio-starters ]**:[ 基于JDK15 + Spring Boot 2.4 + Sa-Token + Mybatis-Plus的单体Boot版脚手架和微服务Cloud版脚手架,带有配套后台管理前端模板及代码生成器](https://gitee.com/uncarbon97/helio-starters)
|
||||
|
||||
## 使用Sa-Token的开源项目
|
||||
[**[ sa-plus ]** 一个基于springboot架构的快速开发框架,内置代码生成器](https://gitee.com/click33/sa-plus)
|
||||
- **[ sa-token-plugin ]**:[Sa-Token第三方插件实现,基于Sa-Token-Core,提供一些与官方不同实现机制的的插件集合,作为Sa-Token开源生态的补充](https://gitee.com/bootx/sa-token-plugin)
|
||||
|
||||
[**[ jthink ]** 一个基于springboot+sa-token+thymeleaf的博客系统](https://gitee.com/wtsoftware/jthink)
|
||||
|
||||
[**[ dcy-fast ]** 一个基于springboot+sa-token+mybatis-plus的后台管理系统,前端vue-element-admin,并且内置代码生成器](https://gitee.com/dcy421/dcy-fast)
|
||||
- **[ easy-admin ]**:[一个基于SpringBoot2 + Sa-Token + Mybatis-Plus + Snakerflow + Layui 的后台管理系统,灵活多变可前后端分离,也可单体,内置代码生成器、权限管理、工作流引擎等](https://gitee.com/lakernote/easy-admin)
|
||||
|
||||
如果您的项目使用了Sa-Token,欢迎提交pr
|
||||
|
||||
|
||||
## 友情链接
|
||||
[**[ okhttps ]** 一个轻量级http通信框架,API设计无比优雅,支持 WebSocket 以及 Stomp 协议](https://gitee.com/ejlchina-zhxu/okhttps)
|
||||
- **[ OkHttps ]**:[ 一个轻量级http通信框架,API设计无比优雅,支持 WebSocket 以及 Stomp 协议](https://gitee.com/ejlchina-zhxu/okhttps)
|
||||
|
||||
- **[ 小诺快速开发平台 ]**:[ 基于SpringBoot2 + AntDesignVue全新快速开发平台,同时拥有三个版本](https://xiaonuo.vip/index#pricing)
|
||||
|
||||
- **[ Jpom ]**:[ 简而轻的低侵入式在线构建、自动部署、日常运维、项目监控软件](https://gitee.com/dromara/Jpom)
|
||||
|
||||
- **[ TLog ]**:[ 一个轻量级的分布式日志标记追踪神器](https://gitee.com/dromara/TLog)
|
||||
|
||||
[**[ 小诺快速开发平台 ]** 基于SpringBoot2 + AntDesignVue全新快速开发平台,同时拥有三个版本](https://xiaonuo.vip/index#pricing)
|
||||
|
||||
|
||||
## 交流群
|
||||
@@ -189,8 +196,10 @@ QQ交流群:1002350610 [点击加入](https://jq.qq.com/?_wv=1027&k=45H977HM)
|
||||
|
||||
微信交流群:
|
||||
|
||||

|
||||
<!--
|
||||

|
||||
|
||||
(扫码添加微信,备注:sa-token,邀您加入群聊)
|
||||
|
||||
-->
|
||||
<br>
|
||||
|
||||
@@ -41,6 +41,14 @@ cd sa-token-demo-sso1
|
||||
call mvn clean
|
||||
cd ..
|
||||
|
||||
cd sa-token-demo-sso1-server
|
||||
call mvn clean
|
||||
cd ..
|
||||
|
||||
cd sa-token-demo-sso1-client
|
||||
call mvn clean
|
||||
cd ..
|
||||
|
||||
cd sa-token-demo-sso2-server
|
||||
call mvn clean
|
||||
cd ..
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-parent</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>1.21.0</version>
|
||||
<version>1.26.0</version>
|
||||
|
||||
<!-- 项目介绍 -->
|
||||
<name>sa-token</name>
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<!-- 一些属性 -->
|
||||
<properties>
|
||||
<sa-token-version>1.21.0</sa-token-version>
|
||||
<sa-token-version>1.26.0</sa-token-version>
|
||||
<jdk.version>1.8</jdk.version>
|
||||
<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>utf-8</project.reporting.outputEncoding>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-parent</artifactId>
|
||||
<version>1.21.0</version>
|
||||
<version>1.26.0</version>
|
||||
</parent>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<description>A Java Web lightweight authority authentication framework, comprehensive function, easy to use</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- Zero dependence! -->
|
||||
<!-- Zero dependence -->
|
||||
</dependencies>
|
||||
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import cn.dev33.satoken.SaManager;
|
||||
import cn.dev33.satoken.annotation.SaCheckBasic;
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaCheckRole;
|
||||
import cn.dev33.satoken.annotation.SaCheckSafe;
|
||||
import cn.dev33.satoken.basic.SaBasicUtil;
|
||||
import cn.dev33.satoken.session.SaSession;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
import cn.dev33.satoken.util.SaTokenConsts;
|
||||
@@ -133,6 +135,12 @@ public class SaTokenActionDefaultImpl implements SaTokenAction {
|
||||
SaCheckSafe at = target.getAnnotation(SaCheckSafe.class);
|
||||
SaManager.getStpLogic(null).checkByAnnotation(at);
|
||||
}
|
||||
|
||||
// 校验 @SaCheckBasic 注解
|
||||
if(target.isAnnotationPresent(SaCheckBasic.class)) {
|
||||
SaCheckBasic at = target.getAnnotation(SaCheckBasic.class);
|
||||
SaBasicUtil.check(at.realm(), at.account());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.dev33.satoken.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import cn.dev33.satoken.basic.SaBasicTemplate;
|
||||
|
||||
/**
|
||||
* Http Basic 认证:只有通过 Basic 认证后才能进入该方法
|
||||
* <p> 可标注在函数、类上(效果等同于标注在此类的所有方法上)
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
public @interface SaCheckBasic {
|
||||
|
||||
/**
|
||||
* 领域
|
||||
* @return see note
|
||||
*/
|
||||
String realm() default SaBasicTemplate.DEFAULT_REALM;
|
||||
|
||||
/**
|
||||
* 需要校验的账号密码
|
||||
* @return see note
|
||||
*/
|
||||
String account() default "";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package cn.dev33.satoken.basic;
|
||||
|
||||
import cn.dev33.satoken.SaManager;
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.exception.NotBasicAuthException;
|
||||
import cn.dev33.satoken.secure.SaBase64Util;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
|
||||
/**
|
||||
* Sa-Token Http Basic 认证模块
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
public class SaBasicTemplate {
|
||||
|
||||
/**
|
||||
* 默认的 Realm 名称
|
||||
*/
|
||||
public static final String DEFAULT_REALM = "Sa-Token";
|
||||
|
||||
/**
|
||||
* 设置响应头,并抛出异常
|
||||
* @param realm 领域
|
||||
*/
|
||||
public void throwNotBasicAuthException(String realm) {
|
||||
SaHolder.getResponse().setStatus(401).setHeader("WWW-Authenticate", "Basic Realm=" + realm);
|
||||
throw new NotBasicAuthException();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器提交的 Basic 参数 (裁剪掉前缀并解码)
|
||||
* @return 值
|
||||
*/
|
||||
public String getAuthorizationValue() {
|
||||
|
||||
// 获取请求头 Authorization 参数
|
||||
String authorization = SaHolder.getRequest().getHeader("Authorization");
|
||||
|
||||
// 如果不是以 Basic 作为前缀,则视为无效
|
||||
if(authorization == null || authorization.startsWith("Basic ") == false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 裁剪前缀并解码
|
||||
return SaBase64Util.decode(authorization.substring(6));
|
||||
}
|
||||
|
||||
/**
|
||||
* 对当前会话进行 Basic 校验(使用全局配置的账号密码),校验不通过则抛出异常
|
||||
*/
|
||||
public void check() {
|
||||
check(DEFAULT_REALM, SaManager.getConfig().getBasic());
|
||||
}
|
||||
|
||||
/**
|
||||
* 对当前会话进行 Basic 校验(手动设置账号密码),校验不通过则抛出异常
|
||||
* @param account 账号(格式为 user:password)
|
||||
*/
|
||||
public void check(String account) {
|
||||
check(DEFAULT_REALM, account);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对当前会话进行 Basic 校验(手动设置 Realm 和 账号密码),校验不通过则抛出异常
|
||||
* @param realm 领域
|
||||
* @param account 账号(格式为 user:password)
|
||||
*/
|
||||
public void check(String realm, String account) {
|
||||
if(SaFoxUtil.isEmpty(account)) {
|
||||
account = SaManager.getConfig().getBasic();
|
||||
}
|
||||
String authorization = getAuthorizationValue();
|
||||
if(SaFoxUtil.isEmpty(authorization) || authorization.equals(account) == false) {
|
||||
throwNotBasicAuthException(realm);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package cn.dev33.satoken.basic;
|
||||
|
||||
/**
|
||||
* Sa-Token Http Basic 认证 Util
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
public class SaBasicUtil {
|
||||
|
||||
/**
|
||||
* 底层 SaBasicTemplate 对象
|
||||
*/
|
||||
public static SaBasicTemplate saBasicTemplate = new SaBasicTemplate();
|
||||
|
||||
/**
|
||||
* 获取浏览器提交的 Basic 参数 (裁剪掉前缀并解码)
|
||||
* @return 值
|
||||
*/
|
||||
public static String getAuthorizationValue() {
|
||||
return saBasicTemplate.getAuthorizationValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对当前会话进行 Basic 校验(使用全局配置的账号密码),校验不通过则抛出异常
|
||||
*/
|
||||
public static void check() {
|
||||
saBasicTemplate.check();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对当前会话进行 Basic 校验(手动设置账号密码),校验不通过则抛出异常
|
||||
* @param account 账号(格式为 user:password)
|
||||
*/
|
||||
public static void check(String account) {
|
||||
saBasicTemplate.check(account);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对当前会话进行 Basic 校验(手动设置 Realm 和 账号密码),校验不通过则抛出异常
|
||||
* @param realm 领域
|
||||
* @param account 账号(格式为 user:password)
|
||||
*/
|
||||
public static void check(String realm, String account) {
|
||||
saBasicTemplate.check(realm, account);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,22 @@
|
||||
package cn.dev33.satoken.config;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import cn.dev33.satoken.exception.SaTokenException;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
|
||||
/**
|
||||
* Sa-Token-SSO 单点登录模块 配置Model
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
public class SaSsoConfig {
|
||||
public class SaSsoConfig implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -6541180061782004705L;
|
||||
|
||||
/**
|
||||
* Ticket有效期 (单位: 秒)
|
||||
@@ -39,6 +48,16 @@ public class SaSsoConfig {
|
||||
*/
|
||||
public String sloUrl;
|
||||
|
||||
/**
|
||||
* SSO-Client端 当前Client端的单点注销回调URL (为空时自动获取)
|
||||
*/
|
||||
public String ssoLogoutCall;
|
||||
|
||||
/**
|
||||
* SSO-Server端 账号资料查询地址
|
||||
*/
|
||||
public String userinfoUrl;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@@ -98,9 +117,11 @@ public class SaSsoConfig {
|
||||
|
||||
/**
|
||||
* @param authUrl SSO-Server端 单点登录地址
|
||||
* @return 对象自身
|
||||
*/
|
||||
public void setAuthUrl(String authUrl) {
|
||||
public SaSsoConfig setAuthUrl(String authUrl) {
|
||||
this.authUrl = authUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,9 +133,11 @@ public class SaSsoConfig {
|
||||
|
||||
/**
|
||||
* @param checkTicketUrl SSO-Server端Ticket校验地址
|
||||
* @return 对象自身
|
||||
*/
|
||||
public void setCheckTicketUrl(String checkTicketUrl) {
|
||||
public SaSsoConfig setCheckTicketUrl(String checkTicketUrl) {
|
||||
this.checkTicketUrl = checkTicketUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,15 +149,50 @@ public class SaSsoConfig {
|
||||
|
||||
/**
|
||||
* @param sloUrl SSO-Server端单点注销地址
|
||||
* @return 对象自身
|
||||
*/
|
||||
public void setSloUrl(String sloUrl) {
|
||||
public SaSsoConfig setSloUrl(String sloUrl) {
|
||||
this.sloUrl = sloUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SSO-Client端 当前Client端的单点注销回调URL (为空时自动获取)
|
||||
*/
|
||||
public String getSsoLogoutCall() {
|
||||
return ssoLogoutCall;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ssoLogoutCall SSO-Client端 当前Client端的单点注销回调URL (为空时自动获取)
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaSsoConfig setSsoLogoutCall(String ssoLogoutCall) {
|
||||
this.ssoLogoutCall = ssoLogoutCall;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SSO-Server端 账号资料查询地址
|
||||
*/
|
||||
public String getUserinfoUrl() {
|
||||
return userinfoUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param userinfoUrl SSO-Server端 账号资料查询地址
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaSsoConfig setUserinfoUrl(String userinfoUrl) {
|
||||
this.userinfoUrl = userinfoUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SaSsoConfig [ticketTimeout=" + ticketTimeout + ", allowUrl=" + allowUrl + ", secretkey=" + secretkey
|
||||
+ ", authUrl=" + authUrl + ", checkTicketUrl=" + checkTicketUrl + ", sloUrl=" + sloUrl + "]";
|
||||
+ ", authUrl=" + authUrl + ", checkTicketUrl=" + checkTicketUrl + ", sloUrl=" + sloUrl
|
||||
+ ", ssoLogoutCall=" + ssoLogoutCall + ", userinfoUrl=" + userinfoUrl + ", isHttp=" + isHttp + ", isSlo=" + isSlo + "]";
|
||||
}
|
||||
|
||||
|
||||
@@ -147,5 +205,111 @@ public class SaSsoConfig {
|
||||
this.allowUrl = SaFoxUtil.arrayJoin(url);
|
||||
return this;
|
||||
}
|
||||
|
||||
// -------------------- SaSsoHandle 相关配置 --------------------
|
||||
|
||||
/**
|
||||
* 是否使用http请求校验ticket值
|
||||
*/
|
||||
public Boolean isHttp = false;
|
||||
|
||||
/**
|
||||
* 是否打开单点注销
|
||||
*/
|
||||
public Boolean isSlo = false;
|
||||
|
||||
|
||||
/**
|
||||
* @return isHttp 是否使用http请求校验ticket值
|
||||
*/
|
||||
public Boolean getIsHttp() {
|
||||
return isHttp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isHttp 是否使用http请求校验ticket值
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaSsoConfig setIsHttp(Boolean isHttp) {
|
||||
this.isHttp = isHttp;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 是否打开单点注销
|
||||
*/
|
||||
public Boolean getIsSlo() {
|
||||
return isSlo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isSlo 是否打开单点注销
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaSsoConfig setIsSlo(Boolean isSlo) {
|
||||
this.isSlo = isSlo;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// -------------------- SaSsoHandle 所有回调函数 --------------------
|
||||
|
||||
|
||||
/**
|
||||
* SSO-Server端:未登录时返回的View
|
||||
*/
|
||||
public Supplier<Object> notLoginView = () -> "当前会话在SSO-Server认证中心尚未登录";
|
||||
|
||||
/**
|
||||
* SSO-Server端:登录函数
|
||||
*/
|
||||
public BiFunction<String, String, Object> doLoginHandle = (name, pwd) -> SaResult.error();
|
||||
|
||||
/**
|
||||
* SSO-Client端:自定义校验Ticket返回值的处理逻辑 (每次从认证中心获取校验Ticket的结果后调用)
|
||||
*/
|
||||
public BiFunction<Object, String, Object> ticketResultHandle = null;
|
||||
|
||||
/**
|
||||
* SSO-Client端:发送Http请求的处理函数
|
||||
*/
|
||||
public Function<String, Object> sendHttp = url -> {throw new SaTokenException("请配置Http处理器");};
|
||||
|
||||
|
||||
/**
|
||||
* @param notLoginView SSO-Server端:未登录时返回的View
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaSsoConfig setNotLoginView(Supplier<Object> notLoginView) {
|
||||
this.notLoginView = notLoginView;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param doLoginHandle SSO-Server端:登录函数
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaSsoConfig setDoLoginHandle(BiFunction<String, String, Object> doLoginHandle) {
|
||||
this.doLoginHandle = doLoginHandle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ticketResultHandle SSO-Client端:自定义校验Ticket返回值的处理逻辑 (每次从认证中心获取校验Ticket的结果后调用)
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaSsoConfig setTicketResultHandle(BiFunction<Object, String, Object> ticketResultHandle) {
|
||||
this.ticketResultHandle = ticketResultHandle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sendHttp SSO-Client端:发送Http请求的处理函数
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaSsoConfig setSendHttp(Function<String, Object> sendHttp) {
|
||||
this.sendHttp = sendHttp;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package cn.dev33.satoken.config;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Sa-Token 配置类 Model
|
||||
* <p>
|
||||
@@ -8,13 +10,15 @@ package cn.dev33.satoken.config;
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
public class SaTokenConfig {
|
||||
public class SaTokenConfig implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -6541180061782004705L;
|
||||
|
||||
/** token名称 (同时也是cookie名称) */
|
||||
private String tokenName = "satoken";
|
||||
|
||||
/** token的长久有效期(单位:秒) 默认30天, -1代表永久 */
|
||||
private long timeout = 30 * 24 * 60 * 60;
|
||||
private long timeout = 60 * 60 * 24 * 30;
|
||||
|
||||
/**
|
||||
* token临时有效期 [指定时间内无操作就视为token过期] (单位: 秒), 默认-1 代表不限制
|
||||
@@ -66,6 +70,20 @@ public class SaTokenConfig {
|
||||
*/
|
||||
private String jwtSecretKey;
|
||||
|
||||
/**
|
||||
* Id-Token的有效期 (单位: 秒)
|
||||
*/
|
||||
private long idTokenTimeout = 60 * 60 * 24;
|
||||
|
||||
/**
|
||||
* Http Basic 认证的账号和密码
|
||||
*/
|
||||
private String basic = "";
|
||||
|
||||
/** 配置当前项目的网络访问地址 */
|
||||
private String currDomain;
|
||||
|
||||
|
||||
/**
|
||||
* SSO单点登录配置对象
|
||||
*/
|
||||
@@ -348,6 +366,54 @@ public class SaTokenConfig {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Id-Token的有效期 (单位: 秒)
|
||||
*/
|
||||
public long getIdTokenTimeout() {
|
||||
return idTokenTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param idTokenTimeout Id-Token的有效期 (单位: 秒)
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaTokenConfig setIdTokenTimeout(long idTokenTimeout) {
|
||||
this.idTokenTimeout = idTokenTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Http Basic 认证的账号和密码
|
||||
*/
|
||||
public String getBasic() {
|
||||
return basic;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param basic Http Basic 认证的账号和密码
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaTokenConfig setBasic(String basic) {
|
||||
this.basic = basic;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 配置当前项目的网络访问地址
|
||||
*/
|
||||
public String getCurrDomain() {
|
||||
return currDomain;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param currDomain 配置当前项目的网络访问地址
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaTokenConfig setCurrDomain(String currDomain) {
|
||||
this.currDomain = currDomain;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SSO单点登录配置对象
|
||||
*/
|
||||
@@ -355,7 +421,6 @@ public class SaTokenConfig {
|
||||
return sso;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param sso SSO单点登录配置对象
|
||||
*/
|
||||
@@ -363,24 +428,18 @@ public class SaTokenConfig {
|
||||
this.sso = sso;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* toString()
|
||||
*/
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SaTokenConfig [tokenName=" + tokenName + ", timeout=" + timeout + ", activityTimeout=" + activityTimeout
|
||||
+ ", isConcurrent=" + isConcurrent + ", isShare=" + isShare + ", isReadBody="
|
||||
+ isReadBody + ", isReadHead=" + isReadHead + ", isReadCookie=" + isReadCookie + ", tokenStyle="
|
||||
+ tokenStyle + ", dataRefreshPeriod=" + dataRefreshPeriod + ", tokenSessionCheckLogin="
|
||||
+ tokenSessionCheckLogin + ", autoRenew=" + autoRenew + ", cookieDomain=" + cookieDomain
|
||||
+ ", tokenPrefix=" + tokenPrefix + ", isPrint=" + isPrint + ", isLog=" + isLog + ", jwtSecretKey="
|
||||
+ jwtSecretKey + ", sso=" + sso + "]";
|
||||
+ ", isConcurrent=" + isConcurrent + ", isShare=" + isShare + ", isReadBody=" + isReadBody
|
||||
+ ", isReadHead=" + isReadHead + ", isReadCookie=" + isReadCookie + ", tokenStyle=" + tokenStyle
|
||||
+ ", dataRefreshPeriod=" + dataRefreshPeriod + ", tokenSessionCheckLogin=" + tokenSessionCheckLogin
|
||||
+ ", autoRenew=" + autoRenew + ", cookieDomain=" + cookieDomain + ", tokenPrefix=" + tokenPrefix
|
||||
+ ", isPrint=" + isPrint + ", isLog=" + isLog + ", jwtSecretKey=" + jwtSecretKey + ", idTokenTimeout="
|
||||
+ idTokenTimeout + ", basic=" + basic + ", currDomain=" + currDomain + ", sso=" + sso + "]";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* <h1> 本函数设计已过时,未来版本可能移除此函数,请及时更换为 setIsConcurrent() ,使用方式保持不变 </h1>
|
||||
* @param allowConcurrentLogin see note
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package cn.dev33.satoken.context.model;
|
||||
|
||||
import cn.dev33.satoken.exception.SaTokenException;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
|
||||
/**
|
||||
* Request 包装类
|
||||
* @author kong
|
||||
@@ -18,8 +21,56 @@ public interface SaRequest {
|
||||
* @param name 键
|
||||
* @return 值
|
||||
*/
|
||||
public String getParameter(String name);
|
||||
public String getParam(String name);
|
||||
|
||||
/**
|
||||
* 在 [请求体] 里获取一个值,值为空时返回默认值
|
||||
* @param name 键
|
||||
* @param defaultValue 值为空时的默认值
|
||||
* @return 值
|
||||
*/
|
||||
public default String getParam(String name, String defaultValue) {
|
||||
String value = getParam(name);
|
||||
if(SaFoxUtil.isEmpty(value)) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测提供的参数是否为指定值
|
||||
* @param name 键
|
||||
* @param value 值
|
||||
* @return 是否相等
|
||||
*/
|
||||
public default boolean isParam(String name, String value) {
|
||||
String paramValue = getParam(name);
|
||||
return SaFoxUtil.isNotEmpty(paramValue) && paramValue.equals(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测请求是否提供了指定参数
|
||||
* @param name 参数名称
|
||||
* @return 是否提供
|
||||
*/
|
||||
public default boolean hasParam(String name) {
|
||||
return SaFoxUtil.isNotEmpty(getParam(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 [请求体] 里获取一个值 (此值必须存在,否则抛出异常 )
|
||||
* @param name 键
|
||||
* @return 参数值
|
||||
*/
|
||||
public default String getParamNotNull(String name) {
|
||||
String paramValue = getParam(name);
|
||||
if(SaFoxUtil.isEmpty(paramValue)) {
|
||||
throw new SaTokenException("缺少参数:" + name);
|
||||
}
|
||||
return paramValue;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 在 [请求头] 里获取一个值
|
||||
* @param name 键
|
||||
@@ -27,6 +78,20 @@ public interface SaRequest {
|
||||
*/
|
||||
public String getHeader(String name);
|
||||
|
||||
/**
|
||||
* 在 [请求头] 里获取一个值
|
||||
* @param name 键
|
||||
* @param defaultValue 值为空时的默认值
|
||||
* @return 值
|
||||
*/
|
||||
public default String getHeader(String name, String defaultValue) {
|
||||
String value = getHeader(name);
|
||||
if(SaFoxUtil.isEmpty(value)) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 [Cookie作用域] 里获取一个值
|
||||
* @param name 键
|
||||
@@ -41,7 +106,16 @@ public interface SaRequest {
|
||||
public String getRequestPath();
|
||||
|
||||
/**
|
||||
* 返回当前请求的url,例:http://xxx.com/?id=127
|
||||
* 返回当前请求path是否为指定值
|
||||
* @param path path
|
||||
* @return see note
|
||||
*/
|
||||
public default boolean isPath(String path) {
|
||||
return getRequestPath().equals(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前请求的url,不带query参数,例:http://xxx.com/test
|
||||
* @return see note
|
||||
*/
|
||||
public String getUrl();
|
||||
@@ -52,4 +126,21 @@ public interface SaRequest {
|
||||
*/
|
||||
public String getMethod();
|
||||
|
||||
/**
|
||||
* 此请求是否为Ajax请求
|
||||
* @return see note
|
||||
*/
|
||||
public default boolean isAjax() {
|
||||
return getHeader("X-Requested-With") != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转发请求
|
||||
* @param path 转发地址
|
||||
* @return 任意值
|
||||
*/
|
||||
public default Object forward(String path) {
|
||||
throw new SaTokenException("No implementation");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,6 +28,13 @@ public interface SaResponse {
|
||||
* @param timeout 过期时间 (秒)
|
||||
*/
|
||||
public void addCookie(String name, String value, String path, String domain, int timeout);
|
||||
|
||||
/**
|
||||
* 设置响应状态码
|
||||
* @param sc 响应状态码
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaResponse setStatus(int sc);
|
||||
|
||||
/**
|
||||
* 在响应头里写入一个值
|
||||
@@ -46,4 +53,11 @@ public interface SaResponse {
|
||||
return this.setHeader("Server", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重定向
|
||||
* @param url 重定向地址
|
||||
* @return 任意值
|
||||
*/
|
||||
public Object redirect(String url);
|
||||
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public interface SaTokenDao {
|
||||
* 写入Value,并设定存活时间 (单位: 秒)
|
||||
* @param key 键名称
|
||||
* @param value 值
|
||||
* @param timeout 过期时间
|
||||
* @param timeout 过期时间(值大于0时限时存储,值=-1时永久存储,值=0或小于-2时不存储)
|
||||
*/
|
||||
public void set(String key, String value, long timeout);
|
||||
|
||||
@@ -75,7 +75,7 @@ public interface SaTokenDao {
|
||||
* 写入Object,并设定存活时间 (单位: 秒)
|
||||
* @param key 键名称
|
||||
* @param object 值
|
||||
* @param timeout 存活时间
|
||||
* @param timeout 存活时间 (值大于0时限时存储,值=-1时永久存储,值=0或小于-2时不存储)
|
||||
*/
|
||||
public void setObject(String key, Object object, long timeout);
|
||||
|
||||
|
||||
@@ -45,6 +45,9 @@ public class SaTokenDaoDefaultImpl implements SaTokenDao {
|
||||
|
||||
@Override
|
||||
public void set(String key, String value, long timeout) {
|
||||
if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
|
||||
return;
|
||||
}
|
||||
dataMap.put(key, value);
|
||||
expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));
|
||||
}
|
||||
@@ -84,6 +87,9 @@ public class SaTokenDaoDefaultImpl implements SaTokenDao {
|
||||
|
||||
@Override
|
||||
public void setObject(String key, Object object, long timeout) {
|
||||
if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
|
||||
return;
|
||||
}
|
||||
dataMap.put(key, object);
|
||||
expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package cn.dev33.satoken.exception;
|
||||
|
||||
/**
|
||||
* 一个异常:代表提供的 Id-Token 无效
|
||||
*
|
||||
* @author kong
|
||||
*/
|
||||
public class IdTokenInvalidException extends SaTokenException {
|
||||
|
||||
/**
|
||||
* 序列化版本号
|
||||
*/
|
||||
private static final long serialVersionUID = 6806129545290130144L;
|
||||
|
||||
/**
|
||||
* 一个异常:代表提供的 Id-Token 无效
|
||||
* @param message 异常描述
|
||||
*/
|
||||
public IdTokenInvalidException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.dev33.satoken.exception;
|
||||
|
||||
/**
|
||||
* 一个异常:代表会话未能通过 Http Basic 认证
|
||||
* @author kong
|
||||
*/
|
||||
public class NotBasicAuthException extends SaTokenException {
|
||||
|
||||
/**
|
||||
* 序列化版本号
|
||||
*/
|
||||
private static final long serialVersionUID = 6806129545290130144L;
|
||||
|
||||
/** 异常提示语 */
|
||||
public static final String BE_MESSAGE = "no basic auth";
|
||||
|
||||
/**
|
||||
* 一个异常:代表会话未通过 Http Basic 认证
|
||||
*/
|
||||
public NotBasicAuthException() {
|
||||
super(BE_MESSAGE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package cn.dev33.satoken.id;
|
||||
|
||||
import cn.dev33.satoken.SaManager;
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.exception.IdTokenInvalidException;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
|
||||
/**
|
||||
* Sa-Token-Id 身份凭证模块
|
||||
* <p> 身份凭证的获取与校验,可用于微服务内部调用鉴权
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
public class SaIdTemplate {
|
||||
|
||||
/**
|
||||
* 在 Request 上储存 Id-Token 时建议使用的key
|
||||
*/
|
||||
public static final String ID_TOKEN = "SA_ID_TOKEN";
|
||||
|
||||
// -------------------- 获取 & 校验
|
||||
|
||||
/**
|
||||
* 获取当前Id-Token, 如果不存在,则立即创建并返回
|
||||
* @return 当前token
|
||||
*/
|
||||
public String getToken() {
|
||||
String currentToken = getTokenNh();
|
||||
if(SaFoxUtil.isEmpty(currentToken)) {
|
||||
// 注意这里的自刷新不能做到高并发可用
|
||||
currentToken = refreshToken();
|
||||
}
|
||||
return currentToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断一个Id-Token是否有效
|
||||
* @param token 要验证的token
|
||||
* @return 这个token是否有效
|
||||
*/
|
||||
public boolean isValid(String token) {
|
||||
// 1、 如果传入的token未空,立即返回false
|
||||
if(SaFoxUtil.isEmpty(token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2、 验证当前 Id-Token 及 Past-Id-Token
|
||||
return token.equals(getToken()) || token.equals(getPastTokenNh());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验一个Id-Token是否有效 (如果无效则抛出异常)
|
||||
* @param token 要验证的token
|
||||
*/
|
||||
public void checkToken(String token) {
|
||||
if(isValid(token) == false) {
|
||||
token = (token == null ? "" : token);
|
||||
throw new IdTokenInvalidException("无效Id-Token:" + token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验当前Request提供的Id-Token是否有效 (如果无效则抛出异常)
|
||||
*/
|
||||
public void checkCurrentRequestToken() {
|
||||
checkToken(SaHolder.getRequest().getHeader(ID_TOKEN));
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新一次Id-Token (注意集群环境中不要多个服务重复调用)
|
||||
* @return 新Token
|
||||
*/
|
||||
public String refreshToken() {
|
||||
|
||||
// 1. 先将当前 Id-Token 写入到 Past-Id-Token 中
|
||||
String idToken = getTokenNh();
|
||||
if(SaFoxUtil.isEmpty(idToken) == false) {
|
||||
savePastToken(idToken, getTokenTimeout());
|
||||
}
|
||||
|
||||
// 2. 再刷新当前Id-Token
|
||||
String newIdToken = createToken();
|
||||
saveToken(newIdToken);
|
||||
|
||||
// 3. 返回新的 Id-Token
|
||||
return newIdToken;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------ 保存Token
|
||||
|
||||
/**
|
||||
* 保存Id-Token
|
||||
* @param token token
|
||||
*/
|
||||
public void saveToken(String token) {
|
||||
if(SaFoxUtil.isEmpty(token)) {
|
||||
return;
|
||||
}
|
||||
SaManager.getSaTokenDao().set(splicingTokenSaveKey(), token, SaManager.getConfig().getIdTokenTimeout());
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存Past-Id-Token
|
||||
* @param token token
|
||||
* @param timeout 有效期(单位:秒)
|
||||
*/
|
||||
public void savePastToken(String token, long timeout){
|
||||
if(SaFoxUtil.isEmpty(token)) {
|
||||
return;
|
||||
}
|
||||
SaManager.getSaTokenDao().set(splicingPastTokenSaveKey(), token, timeout);
|
||||
}
|
||||
|
||||
|
||||
// -------------------- 获取Token
|
||||
|
||||
/**
|
||||
* 获取Id-Token,不做任何处理
|
||||
* @return token
|
||||
*/
|
||||
public String getTokenNh() {
|
||||
return SaManager.getSaTokenDao().get(splicingTokenSaveKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Past-Id-Token,不做任何处理
|
||||
* @return token
|
||||
*/
|
||||
public String getPastTokenNh() {
|
||||
return SaManager.getSaTokenDao().get(splicingPastTokenSaveKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Id-Token的剩余有效期 (单位:秒)
|
||||
* @return token
|
||||
*/
|
||||
public long getTokenTimeout() {
|
||||
return SaManager.getSaTokenDao().getTimeout(splicingTokenSaveKey());
|
||||
}
|
||||
|
||||
|
||||
// -------------------- 创建Token
|
||||
|
||||
/**
|
||||
* 创建一个Id-Token
|
||||
* @return Token
|
||||
*/
|
||||
public String createToken() {
|
||||
return SaFoxUtil.getRandomString(60);
|
||||
}
|
||||
|
||||
|
||||
// -------------------- 拼接key
|
||||
|
||||
/**
|
||||
* 拼接key:Id-Token的存储key
|
||||
* @return key
|
||||
*/
|
||||
public String splicingTokenSaveKey() {
|
||||
return SaManager.getConfig().getTokenName() + ":var:id-token";
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接key:Id-Token的存储key
|
||||
* @return key
|
||||
*/
|
||||
public String splicingPastTokenSaveKey() {
|
||||
return SaManager.getConfig().getTokenName() + ":var:past-id-token";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package cn.dev33.satoken.id;
|
||||
|
||||
/**
|
||||
* Sa-Token-Id 身份凭证模块-工具类
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
public class SaIdUtil {
|
||||
|
||||
/**
|
||||
* 在 Request 上储存 Id-Token 时建议使用的key
|
||||
*/
|
||||
public static final String ID_TOKEN = SaIdTemplate.ID_TOKEN;
|
||||
|
||||
/**
|
||||
* 底层 SaIdTemplate 对象
|
||||
*/
|
||||
public static SaIdTemplate saIdTemplate = new SaIdTemplate();
|
||||
|
||||
// -------------------- 获取 & 校验
|
||||
|
||||
/**
|
||||
* 获取当前Id-Token, 如果不存在,则立即创建并返回
|
||||
* @return 当前token
|
||||
*/
|
||||
public static String getToken() {
|
||||
return saIdTemplate.getToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断一个Id-Token是否有效
|
||||
* @param token 要验证的token
|
||||
* @return 这个token是否有效
|
||||
*/
|
||||
public static boolean isValid(String token) {
|
||||
return saIdTemplate.isValid(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验一个Id-Token是否有效 (如果无效则抛出异常)
|
||||
* @param token 要验证的token
|
||||
*/
|
||||
public static void checkToken(String token) {
|
||||
saIdTemplate.checkToken(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验当前Request提供的Id-Token是否有效 (如果无效则抛出异常)
|
||||
*/
|
||||
public static void checkCurrentRequestToken() {
|
||||
saIdTemplate.checkCurrentRequestToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新一次Id-Token (注意集群环境中不要多个服务重复调用)
|
||||
* @return 新Token
|
||||
*/
|
||||
public static String refreshToken() {
|
||||
return saIdTemplate.refreshToken();
|
||||
}
|
||||
|
||||
|
||||
// -------------------- 获取Token
|
||||
|
||||
/**
|
||||
* 获取Id-Token,不做任何处理
|
||||
* @return token
|
||||
*/
|
||||
public static String getTokenNh() {
|
||||
return saIdTemplate.getTokenNh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Past-Id-Token,不做任何处理
|
||||
* @return token
|
||||
*/
|
||||
public static String getPastTokenNh() {
|
||||
return saIdTemplate.getPastTokenNh();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +1,77 @@
|
||||
package cn.dev33.satoken.sso;
|
||||
|
||||
/**
|
||||
* Sa-Token-SSO模块相关常量
|
||||
* Sa-Token-SSO模块相关常量
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
public class SaSsoConsts {
|
||||
|
||||
/** redirect参数名称 */
|
||||
public static final String REDIRECT_NAME = "redirect";
|
||||
/**
|
||||
* 所有API接口
|
||||
* @author kong
|
||||
*/
|
||||
public static final class Api {
|
||||
|
||||
/** SSO-Server端:授权地址 */
|
||||
public static String ssoAuth = "/sso/auth";
|
||||
|
||||
/** SSO-Server端:RestAPI 登录接口 */
|
||||
public static String ssoDoLogin = "/sso/doLogin";
|
||||
|
||||
/** SSO-Server端:校验ticket 获取账号id */
|
||||
public static String ssoCheckTicket = "/sso/checkTicket";
|
||||
|
||||
/** SSO-Server端 (and Client端):单点注销地址 */
|
||||
public static String ssoLogout = "/sso/logout";
|
||||
|
||||
/** SSO-Client端:登录地址 */
|
||||
public static String ssoLogin = "/sso/login";
|
||||
|
||||
/** SSO-Client端:单点注销的回调 */
|
||||
public static String ssoLogoutCall = "/sso/logoutCall";
|
||||
|
||||
}
|
||||
|
||||
/** ticket参数名称 */
|
||||
public static final String TICKET_NAME = "ticket";
|
||||
/**
|
||||
* 所有参数名称
|
||||
* @author kong
|
||||
*/
|
||||
public static final class ParamName {
|
||||
|
||||
/** back参数名称 */
|
||||
public static final String BACK_NAME = "back";
|
||||
/** redirect参数名称 */
|
||||
public static String redirect = "redirect";
|
||||
|
||||
/** ticket参数名称 */
|
||||
public static String ticket = "ticket";
|
||||
|
||||
/** loginId参数名称 */
|
||||
public static final String LOGIN_ID_NAME = "loginId";
|
||||
/** back参数名称 */
|
||||
public static String back = "back";
|
||||
|
||||
/** secretkey参数名称 */
|
||||
public static final String SECRETKEY = "secretkey";
|
||||
|
||||
/** Client端单点注销时-回调URL 参数名称 */
|
||||
public static final String SLO_CALLBACK_NAME = "sloCallback";
|
||||
/** loginId参数名称 */
|
||||
public static String loginId = "loginId";
|
||||
|
||||
/** secretkey参数名称 */
|
||||
public static String secretkey = "secretkey";
|
||||
|
||||
/** Client端单点注销时-回调URL 参数名称 */
|
||||
public static String ssoLogoutCall = "ssoLogoutCall";
|
||||
|
||||
public static String name = "name";
|
||||
public static String pwd = "pwd";
|
||||
|
||||
}
|
||||
|
||||
/** Client端单点注销回调URL的Set集合,存储在Session中使用的key */
|
||||
public static final String SLO_CALLBACK_SET_KEY = "SLO_CALLBACK_SET_KEY_";
|
||||
|
||||
/** 表示OK的返回结果 */
|
||||
public static final String OK = "ok";
|
||||
|
||||
/** 表示自己 */
|
||||
public static final String SELF = "self";
|
||||
|
||||
/** 表示请求没有得到任何有效处理 {msg: "not handle"} */
|
||||
public static final String NOT_HANDLE = "{\"msg\": \"not handle\"}";
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
package cn.dev33.satoken.sso;
|
||||
|
||||
import cn.dev33.satoken.SaManager;
|
||||
import cn.dev33.satoken.config.SaSsoConfig;
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.context.model.SaRequest;
|
||||
import cn.dev33.satoken.context.model.SaResponse;
|
||||
import cn.dev33.satoken.exception.SaTokenException;
|
||||
import cn.dev33.satoken.sso.SaSsoConsts.Api;
|
||||
import cn.dev33.satoken.sso.SaSsoConsts.ParamName;
|
||||
import cn.dev33.satoken.stp.StpLogic;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
|
||||
/**
|
||||
* Sa-Token-SSO 单点登录请求处理类封装
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
public class SaSsoHandle {
|
||||
|
||||
/**
|
||||
* 处理所有Server端请求
|
||||
* @return 处理结果
|
||||
*/
|
||||
public static Object serverRequest() {
|
||||
|
||||
// 获取对象
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
SaSsoConfig cfg = SaManager.getConfig().getSso();
|
||||
|
||||
// ------------------ 路由分发 ------------------
|
||||
|
||||
// SSO-Server端:授权地址
|
||||
if(req.isPath(Api.ssoAuth)) {
|
||||
return ssoAuth();
|
||||
}
|
||||
|
||||
// SSO-Server端:RestAPI 登录接口
|
||||
if(req.isPath(Api.ssoDoLogin)) {
|
||||
return ssoDoLogin();
|
||||
}
|
||||
|
||||
// SSO-Server端:校验ticket 获取账号id
|
||||
if(req.isPath(Api.ssoCheckTicket) && cfg.isHttp) {
|
||||
return ssoCheckTicket();
|
||||
}
|
||||
|
||||
// SSO-Server端:单点注销
|
||||
if(req.isPath(Api.ssoLogout) && cfg.isSlo) {
|
||||
return ssoServerLogout();
|
||||
}
|
||||
|
||||
// 默认返回
|
||||
return SaSsoConsts.NOT_HANDLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSO-Server端:授权地址
|
||||
* @return 处理结果
|
||||
*/
|
||||
public static Object ssoAuth() {
|
||||
// 获取对象
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
SaResponse res = SaHolder.getResponse();
|
||||
SaSsoConfig cfg = SaManager.getConfig().getSso();
|
||||
StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic;
|
||||
|
||||
// ---------- 此处两种情况分开处理:
|
||||
// 情况1:在SSO认证中心尚未登录,则先去登登录
|
||||
if(stpLogic.isLogin() == false) {
|
||||
return cfg.notLoginView.get();
|
||||
}
|
||||
// 情况2:在SSO认证中心已经登录,开始构建授权重定向地址,下放ticket
|
||||
String redirectUrl = SaSsoUtil.buildRedirectUrl(stpLogic.getLoginId(), req.getParam(ParamName.redirect));
|
||||
return res.redirect(redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* SSO-Server端:RestAPI 登录接口
|
||||
* @return 处理结果
|
||||
*/
|
||||
public static Object ssoDoLogin() {
|
||||
// 获取对象
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
SaSsoConfig cfg = SaManager.getConfig().getSso();
|
||||
|
||||
// 处理
|
||||
return cfg.doLoginHandle.apply(req.getParam(ParamName.name), req.getParam(ParamName.pwd));
|
||||
}
|
||||
|
||||
/**
|
||||
* SSO-Server端:校验ticket 获取账号id
|
||||
* @return 处理结果
|
||||
*/
|
||||
public static Object ssoCheckTicket() {
|
||||
// 获取对象
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
|
||||
// 获取参数
|
||||
String ticket = req.getParam(ParamName.ticket);
|
||||
String sloCallback = req.getParam(ParamName.ssoLogoutCall);
|
||||
|
||||
// 校验ticket,获取对应的账号id
|
||||
Object loginId = SaSsoUtil.checkTicket(ticket);
|
||||
|
||||
// 注册此客户端的单点注销回调URL
|
||||
SaSsoUtil.registerSloCallbackUrl(loginId, sloCallback);
|
||||
|
||||
// 返回给Client端
|
||||
return loginId;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSO-Server端:单点注销
|
||||
* @return 处理结果
|
||||
*/
|
||||
public static Object ssoServerLogout() {
|
||||
// 获取对象
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
SaSsoConfig cfg = SaManager.getConfig().getSso();
|
||||
StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic;
|
||||
|
||||
// 获取参数
|
||||
String loginId = req.getParam(ParamName.loginId);
|
||||
String secretkey = req.getParam(ParamName.secretkey);
|
||||
|
||||
// 遍历通知Client端注销会话
|
||||
// SaSsoUtil.singleLogout(secretkey, loginId, url -> cfg.sendHttp.apply(url));
|
||||
// step.1 校验秘钥
|
||||
SaSsoUtil.checkSecretkey(secretkey);
|
||||
|
||||
// step.2 遍历通知Client端注销会话
|
||||
SaSsoUtil.forEachSloUrl(loginId, url -> cfg.sendHttp.apply(url));
|
||||
|
||||
// step.3 Server端注销
|
||||
stpLogic.logoutByTokenValue(stpLogic.getTokenValueByLoginId(loginId));
|
||||
|
||||
// 完成
|
||||
return SaSsoConsts.OK;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理所有Client端请求
|
||||
* @return 处理结果
|
||||
*/
|
||||
public static Object clientRequest() {
|
||||
|
||||
// 获取对象
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
SaSsoConfig cfg = SaManager.getConfig().getSso();
|
||||
|
||||
// ------------------ 路由分发 ------------------
|
||||
|
||||
// ---------- SSO-Client端:登录地址
|
||||
if(req.isPath(Api.ssoLogin)) {
|
||||
return ssoLogin();
|
||||
}
|
||||
|
||||
// ---------- SSO-Client端:单点注销 [模式二]
|
||||
if(req.isPath(Api.ssoLogout) && cfg.isSlo && cfg.isHttp == false) {
|
||||
return ssoLogoutType2();
|
||||
}
|
||||
|
||||
// ---------- SSO-Client端:单点注销 [模式三]
|
||||
if(req.isPath(Api.ssoLogout) && cfg.isSlo && cfg.isHttp) {
|
||||
return ssoLogoutType3();
|
||||
}
|
||||
|
||||
// ---------- SSO-Client端:单点注销的回调 [模式三]
|
||||
if(req.isPath(Api.ssoLogoutCall) && cfg.isSlo && cfg.isHttp) {
|
||||
return ssoLogoutCall();
|
||||
}
|
||||
|
||||
// 默认返回
|
||||
return SaSsoConsts.NOT_HANDLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSO-Client端:登录地址
|
||||
* @return 处理结果
|
||||
*/
|
||||
public static Object ssoLogin() {
|
||||
// 获取对象
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
SaResponse res = SaHolder.getResponse();
|
||||
SaSsoConfig cfg = SaManager.getConfig().getSso();
|
||||
StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic;
|
||||
|
||||
// 获取参数
|
||||
String back = req.getParam(ParamName.back, "/");
|
||||
String ticket = req.getParam(ParamName.ticket);
|
||||
|
||||
// 如果当前Client端已经登录,则无需访问SSO认证中心,可以直接返回
|
||||
if(stpLogic.isLogin()) {
|
||||
return res.redirect(back);
|
||||
}
|
||||
/*
|
||||
* 此时有两种情况:
|
||||
* 情况1:ticket无值,说明此请求是Client端访问,需要重定向至SSO认证中心
|
||||
* 情况2:ticket有值,说明此请求从SSO认证中心重定向而来,需要根据ticket进行登录
|
||||
*/
|
||||
if(ticket == null) {
|
||||
String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(SaHolder.getRequest().getUrl(), back);
|
||||
return res.redirect(serverAuthUrl);
|
||||
} else {
|
||||
// ------- 1、校验ticket,获取账号id
|
||||
Object loginId = null;
|
||||
if(cfg.isHttp) {
|
||||
// 方式1:使用http请求校验ticket
|
||||
String ssoLogoutCall = null;
|
||||
if(cfg.isSlo) {
|
||||
ssoLogoutCall = SaHolder.getRequest().getUrl().replace(Api.ssoLogin, Api.ssoLogoutCall);
|
||||
}
|
||||
String checkUrl = SaSsoUtil.buildCheckTicketUrl(ticket, ssoLogoutCall);
|
||||
Object body = cfg.sendHttp.apply(checkUrl);
|
||||
loginId = (SaFoxUtil.isEmpty(body) ? null : body);
|
||||
} else {
|
||||
// 方式2:直连Redis校验ticket
|
||||
loginId = SaSsoUtil.checkTicket(ticket);
|
||||
}
|
||||
// Be: 如果开发者自定义了处理逻辑
|
||||
if(cfg.ticketResultHandle != null) {
|
||||
return cfg.ticketResultHandle.apply(loginId, back);
|
||||
}
|
||||
// ------- 2、如果loginId有值,说明ticket有效,进行登录并重定向至back地址
|
||||
if(loginId != null ) {
|
||||
stpLogic.login(loginId);
|
||||
return res.redirect(back);
|
||||
} else {
|
||||
// 如果ticket无效:
|
||||
throw new SaTokenException("无效ticket:" + ticket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SSO-Client端:单点注销 [模式二]
|
||||
* @return 处理结果
|
||||
*/
|
||||
public static Object ssoLogoutType2() {
|
||||
// 获取对象
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
SaResponse res = SaHolder.getResponse();
|
||||
StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic;
|
||||
|
||||
// 开始处理
|
||||
stpLogic.logout();
|
||||
|
||||
// 返回
|
||||
return ssoLogoutBack(req, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* SSO-Client端:单点注销 [模式三]
|
||||
* @return 处理结果
|
||||
*/
|
||||
public static Object ssoLogoutType3() {
|
||||
// 获取对象
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
SaResponse res = SaHolder.getResponse();
|
||||
SaSsoConfig cfg = SaManager.getConfig().getSso();
|
||||
StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic;
|
||||
|
||||
// 如果未登录,则无需注销
|
||||
if(stpLogic.isLogin() == false) {
|
||||
return SaResult.ok();
|
||||
}
|
||||
|
||||
// 调用SSO-Server认证中心API,进行注销
|
||||
String url = SaSsoUtil.buildSloUrl(stpLogic.getLoginId());
|
||||
String body = String.valueOf(cfg.sendHttp.apply(url));
|
||||
if(SaSsoConsts.OK.equals(body) == false) {
|
||||
return SaResult.error("单点注销失败");
|
||||
}
|
||||
|
||||
// 返回
|
||||
return ssoLogoutBack(req, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* SSO-Client端:单点注销的回调 [模式三]
|
||||
* @return 处理结果
|
||||
*/
|
||||
public static Object ssoLogoutCall() {
|
||||
// 获取对象
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic;
|
||||
|
||||
// 获取参数
|
||||
String loginId = req.getParam(ParamName.loginId);
|
||||
String secretkey = req.getParam(ParamName.secretkey);
|
||||
|
||||
SaSsoUtil.checkSecretkey(secretkey);
|
||||
stpLogic.logoutByTokenValue(stpLogic.getTokenValueByLoginId(loginId));
|
||||
return SaSsoConsts.OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* 封装:单点注销成功后返回结果
|
||||
* @param req SaRequest对象
|
||||
* @param res SaResponse对象
|
||||
* @return 返回结果
|
||||
*/
|
||||
public static Object ssoLogoutBack(SaRequest req, SaResponse res) {
|
||||
/*
|
||||
* 三种情况:
|
||||
* 1. 有back参数,值为SELF -> 回退一级并刷新
|
||||
* 2. 有back参数,值为url -> 跳转back地址
|
||||
* 3. 无back参数 -> 返回json数据
|
||||
*/
|
||||
String back = req.getParam(ParamName.back);
|
||||
if(SaFoxUtil.isNotEmpty(back)) {
|
||||
if(back.equals(SaSsoConsts.SELF)) {
|
||||
return "<script>if(document.referrer != location.href){ location.replace(document.referrer || '/'); }</script>";
|
||||
}
|
||||
return res.redirect(back);
|
||||
} else {
|
||||
return SaResult.ok("单点注销成功");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,325 +0,0 @@
|
||||
package cn.dev33.satoken.sso;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import cn.dev33.satoken.SaManager;
|
||||
import cn.dev33.satoken.config.SaSsoConfig;
|
||||
import cn.dev33.satoken.exception.SaTokenException;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
|
||||
/**
|
||||
* Sa-Token-SSO 单点登录接口
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
public interface SaSsoInterface {
|
||||
|
||||
/**
|
||||
* 创建一个 Ticket码
|
||||
* @param loginId 账号id
|
||||
* @return 票据
|
||||
*/
|
||||
public default String createTicket(Object loginId) {
|
||||
// 随机一个ticket
|
||||
String ticket = randomTicket(loginId);
|
||||
|
||||
// 保存入库
|
||||
long ticketTimeout = SaManager.getConfig().getSso().getTicketTimeout();
|
||||
SaManager.getSaTokenDao().set(splicingKeyTicketToId(ticket), String.valueOf(loginId), ticketTimeout);
|
||||
SaManager.getSaTokenDao().set(splicingKeyIdToTicket(loginId), String.valueOf(ticket), ticketTimeout);
|
||||
|
||||
// 返回
|
||||
return ticket;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个 Ticket码
|
||||
* @param ticket Ticket码
|
||||
*/
|
||||
public default void deleteTicket(String ticket) {
|
||||
Object loginId = getLoginId(ticket);
|
||||
if(loginId != null) {
|
||||
SaManager.getSaTokenDao().delete(splicingKeyTicketToId(ticket));
|
||||
SaManager.getSaTokenDao().delete(splicingKeyIdToTicket(loginId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL:Server端向Client下放ticke的地址
|
||||
* @param loginId 账号id
|
||||
* @param redirect Client端提供的重定向地址
|
||||
* @return see note
|
||||
*/
|
||||
public default String buildRedirectUrl(Object loginId, String redirect) {
|
||||
// 校验重定向地址
|
||||
checkRedirectUrl(redirect);
|
||||
|
||||
// 删掉旧ticket
|
||||
String oldTicket = SaManager.getSaTokenDao().get(splicingKeyIdToTicket(loginId));
|
||||
if(oldTicket != null) {
|
||||
deleteTicket(oldTicket);
|
||||
}
|
||||
|
||||
// 获取新ticket
|
||||
String ticket = createTicket(loginId);
|
||||
|
||||
// 构建 授权重定向地址
|
||||
redirect = encodeBackParam(redirect);
|
||||
String redirectUrl = SaFoxUtil.joinParam(redirect, SaSsoConsts.TICKET_NAME, ticket);
|
||||
return redirectUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 Ticket码 获取账号id,如果Ticket码无效则返回null
|
||||
* @param ticket Ticket码
|
||||
* @return 账号id
|
||||
*/
|
||||
public default Object getLoginId(String ticket) {
|
||||
if(SaFoxUtil.isEmpty(ticket)) {
|
||||
return null;
|
||||
}
|
||||
return SaManager.getSaTokenDao().get(splicingKeyTicketToId(ticket));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 Ticket码 获取账号id,并转换为指定类型
|
||||
* @param <T> 要转换的类型
|
||||
* @param ticket Ticket码
|
||||
* @param cs 要转换的类型
|
||||
* @return 账号id
|
||||
*/
|
||||
public default <T> T getLoginId(String ticket, Class<T> cs) {
|
||||
return SaFoxUtil.getValueByType(getLoginId(ticket), cs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验ticket码,获取账号id,如果ticket可以有效,则立刻删除
|
||||
* @param ticket Ticket码
|
||||
* @return 账号id
|
||||
*/
|
||||
public default Object checkTicket(String ticket) {
|
||||
Object loginId = getLoginId(ticket);
|
||||
if(loginId != null) {
|
||||
deleteTicket(ticket);
|
||||
}
|
||||
return loginId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验重定向url合法性
|
||||
* @param url 下放ticket的url地址
|
||||
*/
|
||||
public default void checkRedirectUrl(String url) {
|
||||
|
||||
// 1、是否是一个有效的url
|
||||
if(SaFoxUtil.isUrl(url) == false) {
|
||||
throw new SaTokenException("无效回调地址:" + url);
|
||||
}
|
||||
|
||||
// 2、截取掉?后面的部分
|
||||
int qIndex = url.indexOf("?");
|
||||
if(qIndex != -1) {
|
||||
url = url.substring(0, qIndex);
|
||||
}
|
||||
|
||||
// 3、是否在[允许地址列表]之中
|
||||
String authUrl = SaManager.getConfig().getSso().getAllowUrl().replaceAll(" ", "");
|
||||
List<String> authUrlList = Arrays.asList(authUrl.split(","));
|
||||
if(SaManager.getSaTokenAction().hasElement(authUrlList, url) == false) {
|
||||
throw new SaTokenException("非法回调地址:" + url);
|
||||
}
|
||||
|
||||
// 验证通过
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL:Server端 单点登录地址
|
||||
* @param clientLoginUrl Client端登录地址
|
||||
* @param back 回调路径
|
||||
* @return [SSO-Server端-认证地址 ]
|
||||
*/
|
||||
public default String buildServerAuthUrl(String clientLoginUrl, String back) {
|
||||
// 服务端认证地址
|
||||
String serverUrl = SaManager.getConfig().getSso().getAuthUrl();
|
||||
|
||||
// 对back地址编码
|
||||
back = (back == null ? "" : back);
|
||||
back = SaFoxUtil.encodeUrl(back);
|
||||
|
||||
// 拼接最终地址,格式示例:serverAuthUrl = http://xxx.com?redirectUrl=xxx.com?back=xxx.com
|
||||
clientLoginUrl = SaFoxUtil.joinParam(clientLoginUrl, SaSsoConsts.BACK_NAME, back);
|
||||
String serverAuthUrl = SaFoxUtil.joinParam(serverUrl, SaSsoConsts.REDIRECT_NAME, clientLoginUrl);
|
||||
|
||||
// 返回
|
||||
return serverAuthUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对url中的back参数进行URL编码, 解决超链接重定向后参数丢失的bug
|
||||
* @param url url
|
||||
* @return 编码过后的url
|
||||
*/
|
||||
public default String encodeBackParam(String url) {
|
||||
|
||||
// 获取back参数所在位置
|
||||
int index = url.indexOf("?" + SaSsoConsts.BACK_NAME + "=");
|
||||
if(index == -1) {
|
||||
index = url.indexOf("&" + SaSsoConsts.BACK_NAME + "=");
|
||||
if(index == -1) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
// 开始编码
|
||||
int length = SaSsoConsts.BACK_NAME.length() + 2;
|
||||
String back = url.substring(index + length);
|
||||
back = SaFoxUtil.encodeUrl(back);
|
||||
|
||||
// 放回url中
|
||||
url = url.substring(0, index + length) + back;
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机一个 Ticket码
|
||||
* @param loginId 账号id
|
||||
* @return 票据
|
||||
*/
|
||||
public default String randomTicket(Object loginId) {
|
||||
return SaFoxUtil.getRandomString(64);
|
||||
}
|
||||
|
||||
|
||||
// ------------------- SSO 模式三 -------------------
|
||||
|
||||
/**
|
||||
* 校验secretkey秘钥是否有效
|
||||
* @param secretkey 秘钥
|
||||
*/
|
||||
public default void checkSecretkey(String secretkey) {
|
||||
if(secretkey == null || secretkey.isEmpty() || secretkey.equals(SaManager.getConfig().getSso().getSecretkey()) == false) {
|
||||
throw new SaTokenException("无效秘钥:" + secretkey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL:校验ticket的URL
|
||||
* @param ticket ticket码
|
||||
* @param sloCallbackUrl 单点注销时的回调URL
|
||||
* @return 构建完毕的URL
|
||||
*/
|
||||
public default String buildCheckTicketUrl(String ticket, String sloCallbackUrl) {
|
||||
String url = SaManager.getConfig().getSso().getCheckTicketUrl();
|
||||
// 拼接ticket参数
|
||||
url = SaFoxUtil.joinParam(url, SaSsoConsts.TICKET_NAME, ticket);
|
||||
// 拼接单点注销时的回调URL
|
||||
if(sloCallbackUrl != null) {
|
||||
url = SaFoxUtil.joinParam(url, SaSsoConsts.SLO_CALLBACK_NAME, sloCallbackUrl);
|
||||
}
|
||||
// 返回
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为指定账号id注册单点注销回调URL
|
||||
* @param loginId 账号id
|
||||
* @param sloCallbackUrl 单点注销时的回调URL
|
||||
*/
|
||||
public default void registerSloCallbackUrl(Object loginId, String sloCallbackUrl) {
|
||||
if(loginId == null || sloCallbackUrl == null || sloCallbackUrl.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<String> urlSet = StpUtil.getSessionByLoginId(loginId).get(SaSsoConsts.SLO_CALLBACK_SET_KEY, ()-> new HashSet<String>());
|
||||
urlSet.add(sloCallbackUrl);
|
||||
StpUtil.getSessionByLoginId(loginId).set(SaSsoConsts.SLO_CALLBACK_SET_KEY, urlSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* 循环调用Client端单点注销回调
|
||||
* @param loginId 账号id
|
||||
* @param fun 调用方法
|
||||
*/
|
||||
public default void forEachSloUrl(Object loginId, CallSloUrlFunction fun) {
|
||||
String secretkey = SaManager.getConfig().getSso().getSecretkey();
|
||||
Set<String> urlSet = StpUtil.getSessionByLoginId(loginId).get(SaSsoConsts.SLO_CALLBACK_SET_KEY,
|
||||
() -> new HashSet<String>());
|
||||
|
||||
for (String url : urlSet) {
|
||||
// 拼接:login参数、秘钥参数
|
||||
url = SaFoxUtil.joinParam(url, SaSsoConsts.LOGIN_ID_NAME, loginId);
|
||||
url = SaFoxUtil.joinParam(url, SaSsoConsts.SECRETKEY, secretkey);
|
||||
// 调用
|
||||
fun.run(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL:单点注销URL
|
||||
* @param loginId 要注销的账号id
|
||||
* @return 单点注销URL
|
||||
*/
|
||||
public default String buildSloUrl(Object loginId) {
|
||||
SaSsoConfig ssoConfig = SaManager.getConfig().getSso();
|
||||
String url = ssoConfig.getSloUrl();
|
||||
url = SaFoxUtil.joinParam(url, SaSsoConsts.LOGIN_ID_NAME, loginId);
|
||||
url = SaFoxUtil.joinParam(url, SaSsoConsts.SECRETKEY, ssoConfig.getSecretkey());
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定账号单点注销
|
||||
* @param secretkey 校验秘钥
|
||||
* @param loginId 指定账号
|
||||
* @param fun 调用方法
|
||||
*/
|
||||
public default void singleLogout(String secretkey, Object loginId, CallSloUrlFunction fun) {
|
||||
// step.1 校验秘钥
|
||||
checkSecretkey(secretkey);
|
||||
|
||||
// step.2 遍历通知Client端注销会话
|
||||
forEachSloUrl(loginId, fun);
|
||||
|
||||
// step.3 Server端注销
|
||||
// StpUtil.logoutByLoginId(loginId);
|
||||
StpUtil.logoutByTokenValue(StpUtil.getTokenValueByLoginId(loginId));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ------------------- 返回相应key -------------------
|
||||
|
||||
/**
|
||||
* 拼接key:Ticket 查 账号Id
|
||||
* @param ticket
|
||||
* @return key
|
||||
*/
|
||||
public default String splicingKeyTicketToId(String ticket) {
|
||||
return SaManager.getConfig().getTokenName() + ":ticket:" + ticket;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接key:账号Id 反查 Ticket
|
||||
* @param id 账号id
|
||||
* @return key
|
||||
*/
|
||||
public default String splicingKeyIdToTicket(Object id) {
|
||||
return SaManager.getConfig().getTokenName() + ":id-ticket:" + id;
|
||||
}
|
||||
|
||||
|
||||
@FunctionalInterface
|
||||
static interface CallSloUrlFunction{
|
||||
/**
|
||||
* 调用function
|
||||
* @param url 注销回调URL
|
||||
*/
|
||||
public void run(String url);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
package cn.dev33.satoken.sso;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import cn.dev33.satoken.SaManager;
|
||||
import cn.dev33.satoken.config.SaSsoConfig;
|
||||
import cn.dev33.satoken.exception.SaTokenException;
|
||||
import cn.dev33.satoken.session.SaSession;
|
||||
import cn.dev33.satoken.sso.SaSsoConsts.ParamName;
|
||||
import cn.dev33.satoken.stp.StpLogic;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
|
||||
/**
|
||||
* Sa-Token-SSO 单点登录模块
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
public class SaSsoTemplate {
|
||||
|
||||
/**
|
||||
* 单点登录模块使用的 StpLogic 对象
|
||||
*/
|
||||
public StpLogic stpLogic;
|
||||
public SaSsoTemplate(StpLogic stpLogic) {
|
||||
this.stpLogic = stpLogic;
|
||||
}
|
||||
|
||||
// ---------------------- Ticket 操作 ----------------------
|
||||
|
||||
/**
|
||||
* 根据 账号id 创建一个 Ticket码
|
||||
* @param loginId 账号id
|
||||
* @return Ticket码
|
||||
*/
|
||||
public String createTicket(Object loginId) {
|
||||
// 创建 Ticket
|
||||
String ticket = randomTicket(loginId);
|
||||
|
||||
// 保存 Ticket
|
||||
saveTicket(ticket, loginId);
|
||||
saveTicketIndex(ticket, loginId);
|
||||
|
||||
// 返回 Ticket
|
||||
return ticket;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 Ticket
|
||||
* @param ticket ticket码
|
||||
* @param loginId 账号id
|
||||
*/
|
||||
public void saveTicket(String ticket, Object loginId) {
|
||||
long ticketTimeout = SaManager.getConfig().getSso().getTicketTimeout();
|
||||
SaManager.getSaTokenDao().set(splicingTicketSaveKey(ticket), String.valueOf(loginId), ticketTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 Ticket 索引
|
||||
* @param ticket ticket码
|
||||
* @param loginId 账号id
|
||||
*/
|
||||
public void saveTicketIndex(String ticket, Object loginId) {
|
||||
long ticketTimeout = SaManager.getConfig().getSso().getTicketTimeout();
|
||||
SaManager.getSaTokenDao().set(splicingTicketIndexKey(loginId), String.valueOf(ticket), ticketTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 Ticket
|
||||
* @param ticket Ticket码
|
||||
*/
|
||||
public void deleteTicket(String ticket) {
|
||||
if(ticket == null) {
|
||||
return;
|
||||
}
|
||||
SaManager.getSaTokenDao().delete(splicingTicketSaveKey(ticket));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 Ticket索引
|
||||
* @param loginId 账号id
|
||||
*/
|
||||
public void deleteTicketIndex(Object loginId) {
|
||||
if(loginId == null) {
|
||||
return;
|
||||
}
|
||||
SaManager.getSaTokenDao().delete(splicingTicketIndexKey(loginId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 Ticket码 获取账号id,如果Ticket码无效则返回null
|
||||
* @param ticket Ticket码
|
||||
* @return 账号id
|
||||
*/
|
||||
public Object getLoginId(String ticket) {
|
||||
if(SaFoxUtil.isEmpty(ticket)) {
|
||||
return null;
|
||||
}
|
||||
return SaManager.getSaTokenDao().get(splicingTicketSaveKey(ticket));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 Ticket码 获取账号id,并转换为指定类型
|
||||
* @param <T> 要转换的类型
|
||||
* @param ticket Ticket码
|
||||
* @param cs 要转换的类型
|
||||
* @return 账号id
|
||||
*/
|
||||
public <T> T getLoginId(String ticket, Class<T> cs) {
|
||||
return SaFoxUtil.getValueByType(getLoginId(ticket), cs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 指定账号id的 Ticket值
|
||||
* @param loginId 账号id
|
||||
* @return Ticket值
|
||||
*/
|
||||
public String getTicketValue(Object loginId) {
|
||||
if(loginId == null) {
|
||||
return null;
|
||||
}
|
||||
return SaManager.getSaTokenDao().get(splicingTicketIndexKey(loginId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验ticket码,获取账号id,如果此ticket是有效的,则立即删除
|
||||
* @param ticket Ticket码
|
||||
* @return 账号id
|
||||
*/
|
||||
public Object checkTicket(String ticket) {
|
||||
Object loginId = getLoginId(ticket);
|
||||
if(loginId != null) {
|
||||
deleteTicket(ticket);
|
||||
deleteTicketIndex(loginId);
|
||||
}
|
||||
return loginId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机一个 Ticket码
|
||||
* @param loginId 账号id
|
||||
* @return Ticket码
|
||||
*/
|
||||
public String randomTicket(Object loginId) {
|
||||
return SaFoxUtil.getRandomString(64);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------- 构建URL ----------------------
|
||||
|
||||
/**
|
||||
* 构建URL:Server端 单点登录地址
|
||||
* @param clientLoginUrl Client端登录地址
|
||||
* @param back 回调路径
|
||||
* @return [SSO-Server端-认证地址 ]
|
||||
*/
|
||||
public String buildServerAuthUrl(String clientLoginUrl, String back) {
|
||||
|
||||
// 服务端认证地址
|
||||
String serverUrl = SaManager.getConfig().getSso().getAuthUrl();
|
||||
|
||||
// 对back地址编码
|
||||
back = (back == null ? "" : back);
|
||||
back = SaFoxUtil.encodeUrl(back);
|
||||
|
||||
// 拼接最终地址,格式示例:serverAuthUrl = http://xxx.com?redirectUrl=xxx.com?back=xxx.com
|
||||
clientLoginUrl = SaFoxUtil.joinParam(clientLoginUrl, ParamName.back, back);
|
||||
String serverAuthUrl = SaFoxUtil.joinParam(serverUrl, ParamName.redirect, clientLoginUrl);
|
||||
|
||||
// 返回
|
||||
return serverAuthUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL:Server端向Client下放ticke的地址
|
||||
* @param loginId 账号id
|
||||
* @param redirect Client端提供的重定向地址
|
||||
* @return see note
|
||||
*/
|
||||
public String buildRedirectUrl(Object loginId, String redirect) {
|
||||
|
||||
// 校验 重定向地址 是否合法
|
||||
checkRedirectUrl(redirect);
|
||||
|
||||
// 删掉 旧Ticket
|
||||
deleteTicket(getTicketValue(loginId));
|
||||
|
||||
// 创建 新Ticket
|
||||
String ticket = createTicket(loginId);
|
||||
|
||||
// 构建 授权重定向地址 (Server端 根据此地址向 Client端 下放Ticket)
|
||||
return SaFoxUtil.joinParam(encodeBackParam(redirect), ParamName.ticket, ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验重定向url合法性
|
||||
* @param url 下放ticket的url地址
|
||||
*/
|
||||
public void checkRedirectUrl(String url) {
|
||||
|
||||
// 1、是否是一个有效的url
|
||||
if(SaFoxUtil.isUrl(url) == false) {
|
||||
throw new SaTokenException("无效redirect:" + url);
|
||||
}
|
||||
|
||||
// 2、截取掉?后面的部分
|
||||
int qIndex = url.indexOf("?");
|
||||
if(qIndex != -1) {
|
||||
url = url.substring(0, qIndex);
|
||||
}
|
||||
|
||||
// 3、是否在[允许地址列表]之中
|
||||
List<String> authUrlList = Arrays.asList(getAllowUrl().replaceAll(" ", "").split(","));
|
||||
if(SaManager.getSaTokenAction().hasElement(authUrlList, url) == false) {
|
||||
throw new SaTokenException("非法redirect:" + url);
|
||||
}
|
||||
|
||||
// 校验通过 √
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取:所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket)
|
||||
* @return see note
|
||||
*/
|
||||
public String getAllowUrl() {
|
||||
// 默认从配置文件中返回
|
||||
return SaManager.getConfig().getSso().getAllowUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对url中的back参数进行URL编码, 解决超链接重定向后参数丢失的bug
|
||||
* @param url url
|
||||
* @return 编码过后的url
|
||||
*/
|
||||
public String encodeBackParam(String url) {
|
||||
|
||||
// 获取back参数所在位置
|
||||
int index = url.indexOf("?" + ParamName.back + "=");
|
||||
if(index == -1) {
|
||||
index = url.indexOf("&" + ParamName.back + "=");
|
||||
if(index == -1) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
// 开始编码
|
||||
int length = ParamName.back.length() + 2;
|
||||
String back = url.substring(index + length);
|
||||
back = SaFoxUtil.encodeUrl(back);
|
||||
|
||||
// 放回url中
|
||||
url = url.substring(0, index + length) + back;
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL:Server端 账号资料查询地址
|
||||
* @param loginId 账号id
|
||||
* @return Server端 账号资料查询地址
|
||||
*/
|
||||
public String buildUserinfoUrl(Object loginId) {
|
||||
// 拼接
|
||||
String userinfoUrl = SaManager.getConfig().getSso().getUserinfoUrl();
|
||||
userinfoUrl = SaFoxUtil.joinParam(userinfoUrl, ParamName.loginId, loginId);
|
||||
userinfoUrl = SaFoxUtil.joinParam(userinfoUrl, ParamName.secretkey, SaManager.getConfig().getSso().getSecretkey());
|
||||
// 返回
|
||||
return userinfoUrl;
|
||||
}
|
||||
|
||||
|
||||
// ------------------- SSO 模式三相关 -------------------
|
||||
|
||||
/**
|
||||
* 校验secretkey秘钥是否有效
|
||||
* @param secretkey 秘钥
|
||||
*/
|
||||
public void checkSecretkey(String secretkey) {
|
||||
if(secretkey == null || secretkey.isEmpty() || secretkey.equals(SaManager.getConfig().getSso().getSecretkey()) == false) {
|
||||
throw new SaTokenException("无效秘钥:" + secretkey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL:校验ticket的URL
|
||||
* <p> 在模式三下,Client端拿到Ticket后根据此地址向Server端发送请求,获取账号id
|
||||
* @param ticket ticket码
|
||||
* @param ssoLogoutCallUrl 单点注销时的回调URL
|
||||
* @return 构建完毕的URL
|
||||
*/
|
||||
public String buildCheckTicketUrl(String ticket, String ssoLogoutCallUrl) {
|
||||
// 裸地址
|
||||
String url = SaManager.getConfig().getSso().getCheckTicketUrl();
|
||||
|
||||
// 拼接ticket参数
|
||||
url = SaFoxUtil.joinParam(url, ParamName.ticket, ticket);
|
||||
|
||||
// 拼接单点注销时的回调URL
|
||||
if(ssoLogoutCallUrl != null) {
|
||||
url = SaFoxUtil.joinParam(url, ParamName.ssoLogoutCall, ssoLogoutCallUrl);
|
||||
}
|
||||
|
||||
// 返回
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为指定账号id注册单点注销回调URL
|
||||
* @param loginId 账号id
|
||||
* @param sloCallbackUrl 单点注销时的回调URL
|
||||
*/
|
||||
public void registerSloCallbackUrl(Object loginId, String sloCallbackUrl) {
|
||||
if(loginId == null || sloCallbackUrl == null || sloCallbackUrl.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
SaSession session = stpLogic.getSessionByLoginId(loginId);
|
||||
Set<String> urlSet = session.get(SaSsoConsts.SLO_CALLBACK_SET_KEY, ()-> new HashSet<String>());
|
||||
urlSet.add(sloCallbackUrl);
|
||||
session.set(SaSsoConsts.SLO_CALLBACK_SET_KEY, urlSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* 循环调用Client端单点注销回调
|
||||
* @param loginId 账号id
|
||||
* @param fun 调用方法
|
||||
*/
|
||||
public void forEachSloUrl(Object loginId, CallSloUrlFunction fun) {
|
||||
String secretkey = SaManager.getConfig().getSso().getSecretkey();
|
||||
Set<String> urlSet = stpLogic.getSessionByLoginId(loginId).get(SaSsoConsts.SLO_CALLBACK_SET_KEY,
|
||||
() -> new HashSet<String>());
|
||||
|
||||
for (String url : urlSet) {
|
||||
// 拼接:login参数、秘钥参数
|
||||
url = SaFoxUtil.joinParam(url, ParamName.loginId, loginId);
|
||||
url = SaFoxUtil.joinParam(url, ParamName.secretkey, secretkey);
|
||||
// 调用
|
||||
fun.run(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL:单点注销URL
|
||||
* @param loginId 要注销的账号id
|
||||
* @return 单点注销URL
|
||||
*/
|
||||
public String buildSloUrl(Object loginId) {
|
||||
SaSsoConfig ssoConfig = SaManager.getConfig().getSso();
|
||||
String url = ssoConfig.getSloUrl();
|
||||
url = SaFoxUtil.joinParam(url, ParamName.loginId, loginId);
|
||||
url = SaFoxUtil.joinParam(url, ParamName.secretkey, ssoConfig.getSecretkey());
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定账号单点注销
|
||||
* @param secretkey 校验秘钥
|
||||
* @param loginId 指定账号
|
||||
* @param fun 调用方法
|
||||
*/
|
||||
public void singleLogout(String secretkey, Object loginId, CallSloUrlFunction fun) {
|
||||
// step.1 校验秘钥
|
||||
checkSecretkey(secretkey);
|
||||
|
||||
// step.2 遍历通知Client端注销会话
|
||||
forEachSloUrl(loginId, fun);
|
||||
|
||||
// step.3 Server端注销
|
||||
// StpUtil.logoutByLoginId(loginId);
|
||||
stpLogic.logoutByTokenValue(stpLogic.getTokenValueByLoginId(loginId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取:账号资料
|
||||
* @param loginId 账号id
|
||||
* @return 账号资料
|
||||
*/
|
||||
public Object getUserinfo(Object loginId) {
|
||||
String url = buildUserinfoUrl(loginId);
|
||||
return SaManager.getConfig().getSso().sendHttp.apply(url);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ------------------- 返回相应key -------------------
|
||||
|
||||
/**
|
||||
* 拼接key:Ticket 查 账号Id
|
||||
* @param ticket ticket值
|
||||
* @return key
|
||||
*/
|
||||
public String splicingTicketSaveKey(String ticket) {
|
||||
return SaManager.getConfig().getTokenName() + ":ticket:" + ticket;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接key:账号Id 反查 Ticket
|
||||
* @param id 账号id
|
||||
* @return key
|
||||
*/
|
||||
public String splicingTicketIndexKey(Object id) {
|
||||
return SaManager.getConfig().getTokenName() + ":id-ticket:" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单点注销回调函数
|
||||
* @author kong
|
||||
*/
|
||||
@FunctionalInterface
|
||||
static interface CallSloUrlFunction{
|
||||
/**
|
||||
* 调用function
|
||||
* @param url 注销回调URL
|
||||
*/
|
||||
public void run(String url);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,53 +1,55 @@
|
||||
package cn.dev33.satoken.sso;
|
||||
|
||||
import cn.dev33.satoken.sso.SaSsoInterface.CallSloUrlFunction;
|
||||
import cn.dev33.satoken.sso.SaSsoTemplate.CallSloUrlFunction;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
|
||||
/**
|
||||
* Sa-Token-SSO 单点登录工具类
|
||||
* Sa-Token-SSO 单点登录模块 工具类
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
public class SaSsoUtil {
|
||||
|
||||
/**
|
||||
* 底层 SaSsoServerInterface 对象
|
||||
* 底层 SaSsoTemplate 对象
|
||||
*/
|
||||
public static SaSsoInterface saSsoInterface = new SaSsoInterface() {};
|
||||
public static SaSsoTemplate saSsoTemplate = new SaSsoTemplate(StpUtil.stpLogic);
|
||||
|
||||
|
||||
// ---------------------- Ticket 操作 ----------------------
|
||||
|
||||
/**
|
||||
* 创建一个 Ticket票据
|
||||
* 根据 账号id 创建一个 Ticket码
|
||||
* @param loginId 账号id
|
||||
* @return 票据
|
||||
* @return Ticket码
|
||||
*/
|
||||
public static String createTicket(Object loginId) {
|
||||
return saSsoInterface.createTicket(loginId);
|
||||
return saSsoTemplate.createTicket(loginId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个 Ticket码
|
||||
* 删除 Ticket
|
||||
* @param ticket Ticket码
|
||||
*/
|
||||
public static void deleteTicket(String ticket) {
|
||||
saSsoInterface.deleteTicket(ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL:Server端向Client下放ticke的地址
|
||||
* @param loginId 账号id
|
||||
* @param redirect Client端提供的重定向地址
|
||||
* @return see note
|
||||
*/
|
||||
public static String buildRedirectUrl(Object loginId, String redirect) {
|
||||
return saSsoInterface.buildRedirectUrl(loginId, redirect);
|
||||
saSsoTemplate.deleteTicket(ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 Ticket索引
|
||||
* @param loginId 账号id
|
||||
*/
|
||||
public static void deleteTicketIndex(Object loginId) {
|
||||
saSsoTemplate.deleteTicketIndex(loginId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 Ticket码 获取账号id,如果Ticket码无效则返回null
|
||||
* @param ticket Ticket码
|
||||
* @return 账号id
|
||||
*/
|
||||
public static Object getLoginId(String ticket) {
|
||||
return saSsoInterface.getLoginId(ticket);
|
||||
return saSsoTemplate.getLoginId(ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,25 +60,20 @@ public class SaSsoUtil {
|
||||
* @return 账号id
|
||||
*/
|
||||
public static <T> T getLoginId(String ticket, Class<T> cs) {
|
||||
return saSsoInterface.getLoginId(ticket, cs);
|
||||
return saSsoTemplate.getLoginId(ticket, cs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验ticket码,获取账号id,如果ticket可以有效,则立刻删除
|
||||
* 校验ticket码,获取账号id,如果此ticket是有效的,则立即删除
|
||||
* @param ticket Ticket码
|
||||
* @return 账号id
|
||||
*/
|
||||
public static Object checkTicket(String ticket) {
|
||||
return saSsoInterface.checkTicket(ticket);
|
||||
return saSsoTemplate.checkTicket(ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验重定向url合法性
|
||||
* @param url 下放ticket的url地址
|
||||
*/
|
||||
public static void checkAuthUrl(String url) {
|
||||
saSsoInterface.checkRedirectUrl(url);
|
||||
}
|
||||
|
||||
// ---------------------- 构建URL ----------------------
|
||||
|
||||
/**
|
||||
* 构建URL:Server端 单点登录地址
|
||||
@@ -85,9 +82,43 @@ public class SaSsoUtil {
|
||||
* @return [SSO-Server端-认证地址 ]
|
||||
*/
|
||||
public static String buildServerAuthUrl(String clientLoginUrl, String back) {
|
||||
return saSsoInterface.buildServerAuthUrl(clientLoginUrl, back);
|
||||
return saSsoTemplate.buildServerAuthUrl(clientLoginUrl, back);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL:Server端向Client下放ticke的地址
|
||||
* @param loginId 账号id
|
||||
* @param redirect Client端提供的重定向地址
|
||||
* @return see note
|
||||
*/
|
||||
public static String buildRedirectUrl(Object loginId, String redirect) {
|
||||
return saSsoTemplate.buildRedirectUrl(loginId, redirect);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验重定向url合法性
|
||||
* @param url 下放ticket的url地址
|
||||
*/
|
||||
public static void checkRedirectUrl(String url) {
|
||||
saSsoTemplate.checkRedirectUrl(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取:所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket)
|
||||
* @return see note
|
||||
*/
|
||||
public static String getAllowUrl() {
|
||||
return saSsoTemplate.getAllowUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL:Server端 账号资料查询地址
|
||||
* @param loginId 账号id
|
||||
* @return Server端 账号资料查询地址
|
||||
*/
|
||||
public static String buildUserinfoUrl(Object loginId) {
|
||||
return saSsoTemplate.buildUserinfoUrl(loginId);
|
||||
}
|
||||
|
||||
// ------------------- SSO 模式三 -------------------
|
||||
|
||||
@@ -96,17 +127,17 @@ public class SaSsoUtil {
|
||||
* @param secretkey 秘钥
|
||||
*/
|
||||
public static void checkSecretkey(String secretkey) {
|
||||
saSsoInterface.checkSecretkey(secretkey);
|
||||
saSsoTemplate.checkSecretkey(secretkey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL:校验ticket的URL
|
||||
* @param ticket ticket码
|
||||
* @param sloCallbackUrl 单点注销时的回调URL (如果不需要单点注销功能,此值可以填null)
|
||||
* @param ssoLogoutCallUrl 单点注销时的回调URL
|
||||
* @return 构建完毕的URL
|
||||
*/
|
||||
public static String buildCheckTicketUrl(String ticket, String sloCallbackUrl) {
|
||||
return saSsoInterface.buildCheckTicketUrl(ticket, sloCallbackUrl);
|
||||
public static String buildCheckTicketUrl(String ticket, String ssoLogoutCallUrl) {
|
||||
return saSsoTemplate.buildCheckTicketUrl(ticket, ssoLogoutCallUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,7 +146,7 @@ public class SaSsoUtil {
|
||||
* @param sloCallbackUrl 单点注销时的回调URL
|
||||
*/
|
||||
public static void registerSloCallbackUrl(Object loginId, String sloCallbackUrl) {
|
||||
saSsoInterface.registerSloCallbackUrl(loginId, sloCallbackUrl);
|
||||
saSsoTemplate.registerSloCallbackUrl(loginId, sloCallbackUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,7 +155,7 @@ public class SaSsoUtil {
|
||||
* @param fun 调用方法
|
||||
*/
|
||||
public static void forEachSloUrl(Object loginId, CallSloUrlFunction fun) {
|
||||
saSsoInterface.forEachSloUrl(loginId, fun);
|
||||
saSsoTemplate.forEachSloUrl(loginId, fun);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,7 +164,7 @@ public class SaSsoUtil {
|
||||
* @return 单点注销URL
|
||||
*/
|
||||
public static String buildSloUrl(Object loginId) {
|
||||
return saSsoInterface.buildSloUrl(loginId);
|
||||
return saSsoTemplate.buildSloUrl(loginId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +174,16 @@ public class SaSsoUtil {
|
||||
* @param fun 调用方法
|
||||
*/
|
||||
public static void singleLogout(String secretkey, Object loginId, CallSloUrlFunction fun) {
|
||||
saSsoInterface.singleLogout(secretkey, loginId, fun);
|
||||
saSsoTemplate.singleLogout(secretkey, loginId, fun);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取:账号资料
|
||||
* @param loginId 账号id
|
||||
* @return 账号资料
|
||||
*/
|
||||
public static Object getUserinfo(Object loginId) {
|
||||
return saSsoTemplate.getUserinfo(loginId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ public class StpLogic {
|
||||
*/
|
||||
public StpLogic setLoginType(String loginType){
|
||||
this.loginType = loginType;
|
||||
SaManager.putStpLogic(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -132,7 +133,7 @@ public class StpLogic {
|
||||
}
|
||||
// 2. 尝试从请求体里面读取
|
||||
if(tokenValue == null && config.getIsReadBody()){
|
||||
tokenValue = request.getParameter(keyTokenName);
|
||||
tokenValue = request.getParam(keyTokenName);
|
||||
}
|
||||
// 3. 尝试从header里读取
|
||||
if(tokenValue == null && config.getIsReadHead()){
|
||||
@@ -258,13 +259,10 @@ public class StpLogic {
|
||||
tokenValue = createTokenValue(id);
|
||||
}
|
||||
|
||||
// ------ 3. 获取[User-Session] (如果还没有创建session, 则新建, 如果已经创建,则续期)
|
||||
SaSession session = getSessionByLoginId(id, false);
|
||||
if(session == null) {
|
||||
session = getSessionByLoginId(id);
|
||||
} else {
|
||||
session.updateMinTimeout(loginModel.getTimeout());
|
||||
}
|
||||
// ------ 3. 获取[User-Session], 续期
|
||||
SaSession session = getSessionByLoginId(id, true);
|
||||
session.updateMinTimeout(loginModel.getTimeout());
|
||||
|
||||
// 在session上记录token签名
|
||||
session.addTokenSign(new TokenSign(tokenValue, loginModel.getDevice()));
|
||||
|
||||
|
||||
@@ -54,13 +54,22 @@ public class SaFoxUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定字符串是否为null或者空字符串
|
||||
* @param str 指定字符串
|
||||
* 指定元素是否为null或者空字符串
|
||||
* @param str 指定元素
|
||||
* @return 是否为null或者空字符串
|
||||
*/
|
||||
public static boolean isEmpty(String str) {
|
||||
public static boolean isEmpty(Object str) {
|
||||
return str == null || "".equals(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定元素是否不为 (null或者空字符串)
|
||||
* @param str 指定元素
|
||||
* @return 是否为null或者空字符串
|
||||
*/
|
||||
public static boolean isNotEmpty(Object str) {
|
||||
return isEmpty(str) == false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 以当前时间戳和随机int数字拼接一个随机字符串
|
||||
@@ -204,7 +213,7 @@ public class SaFoxUtil {
|
||||
if(url == null) {
|
||||
url = "";
|
||||
}
|
||||
int index = url.indexOf('?');
|
||||
int index = url.lastIndexOf('?');
|
||||
// ? 不存在
|
||||
if(index == -1) {
|
||||
return url + '?' + parameStr;
|
||||
@@ -236,11 +245,63 @@ public class SaFoxUtil {
|
||||
*/
|
||||
public static String joinParam(String url, String key, Object value) {
|
||||
// 如果参数为空, 直接返回
|
||||
if(isEmpty(url) || isEmpty(key) || isEmpty(String.valueOf(value))) {
|
||||
if(isEmpty(url) || isEmpty(key) || isEmpty(value)) {
|
||||
return url;
|
||||
}
|
||||
return joinParam(url, key + "=" + value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在url上拼接锚参数
|
||||
* @param url url
|
||||
* @param parameStr 参数, 例如 id=1001
|
||||
* @return 拼接后的url字符串
|
||||
*/
|
||||
public static String joinSharpParam(String url, String parameStr) {
|
||||
// 如果参数为空, 直接返回
|
||||
if(parameStr == null || parameStr.length() == 0) {
|
||||
return url;
|
||||
}
|
||||
if(url == null) {
|
||||
url = "";
|
||||
}
|
||||
int index = url.lastIndexOf('#');
|
||||
// ? 不存在
|
||||
if(index == -1) {
|
||||
return url + '#' + parameStr;
|
||||
}
|
||||
// ? 是最后一位
|
||||
if(index == url.length() - 1) {
|
||||
return url + parameStr;
|
||||
}
|
||||
// ? 是其中一位
|
||||
if(index > -1 && index < url.length() - 1) {
|
||||
String separatorChar = "&";
|
||||
// 如果最后一位是 不是&, 且 parameStr 第一位不是 &, 就增送一个 &
|
||||
if(url.lastIndexOf(separatorChar) != url.length() - 1 && parameStr.indexOf(separatorChar) != 0) {
|
||||
return url + separatorChar + parameStr;
|
||||
} else {
|
||||
return url + parameStr;
|
||||
}
|
||||
}
|
||||
// 正常情况下, 代码不可能执行到此
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在url上拼接锚参数
|
||||
* @param url url
|
||||
* @param key 参数名称
|
||||
* @param value 参数值
|
||||
* @return 拼接后的url字符串
|
||||
*/
|
||||
public static String joinSharpParam(String url, String key, Object value) {
|
||||
// 如果参数为空, 直接返回
|
||||
if(isEmpty(url) || isEmpty(key) || isEmpty(value)) {
|
||||
return url;
|
||||
}
|
||||
return joinSharpParam(url, key + "=" + value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数组的所有元素使用逗号拼接在一起
|
||||
@@ -304,4 +365,43 @@ public class SaFoxUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定字符串按照逗号分隔符转化为字符串集合
|
||||
* @param str 字符串
|
||||
* @return 分割后的字符串集合
|
||||
*/
|
||||
public static List<String> convertStringToList(String str) {
|
||||
List<String> list = new ArrayList<String>();
|
||||
if(isEmpty(str)) {
|
||||
return list;
|
||||
}
|
||||
String[] arr = str.split(",");
|
||||
for (String s : arr) {
|
||||
s = s.trim();
|
||||
if(isEmpty(s) == false) {
|
||||
list.add(s);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定集合按照逗号连接成一个字符串
|
||||
* @param list 集合
|
||||
* @return 字符串
|
||||
*/
|
||||
public static String convertListToString(List<?> list) {
|
||||
if(list == null || list.size() == 0) {
|
||||
return "";
|
||||
}
|
||||
String str = "";
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
str += list.get(i);
|
||||
if(i != list.size() - 1) {
|
||||
str += ",";
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package cn.dev33.satoken.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 对Ajax请求返回Json格式数据的简易封装 <br>
|
||||
* 所有预留字段:<br>
|
||||
* code=状态码 <br>
|
||||
* msg=描述信息 <br>
|
||||
* data=携带对象 <br>
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
public class SaResult extends LinkedHashMap<String, Object> implements Serializable{
|
||||
|
||||
private static final long serialVersionUID = 1L; // 序列化版本号
|
||||
|
||||
public static final int CODE_SUCCESS = 200;
|
||||
public static final int CODE_ERROR = 500;
|
||||
|
||||
public SaResult(int code, String msg, Object data) {
|
||||
this.setCode(code);
|
||||
this.setMsg(msg);
|
||||
this.setData(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取code
|
||||
* @return code
|
||||
*/
|
||||
public Integer getCode() {
|
||||
return (Integer)this.get("code");
|
||||
}
|
||||
/**
|
||||
* 获取msg
|
||||
* @return msg
|
||||
*/
|
||||
public String getMsg() {
|
||||
return (String)this.get("msg");
|
||||
}
|
||||
/**
|
||||
* 获取data
|
||||
* @return data
|
||||
*/
|
||||
public Object getData() {
|
||||
return (Object)this.get("data");
|
||||
}
|
||||
|
||||
/**
|
||||
* 给code赋值,连缀风格
|
||||
* @param code code
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaResult setCode(int code) {
|
||||
this.put("code", code);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* 给msg赋值,连缀风格
|
||||
* @param msg msg
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaResult setMsg(String msg) {
|
||||
this.put("msg", msg);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* 给data赋值,连缀风格
|
||||
* @param data data
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaResult setData(Object data) {
|
||||
this.put("data", data);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入一个值 自定义key, 连缀风格
|
||||
* @param key key
|
||||
* @param data data
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaResult set(String key, Object data) {
|
||||
this.put(key, data);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入一个Map, 连缀风格
|
||||
* @param map map
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaResult setMap(Map<String, ?> map) {
|
||||
for (String key : map.keySet()) {
|
||||
this.put(key, map.get(key));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// ============================ 构建 ==================================
|
||||
|
||||
// 构建成功
|
||||
public static SaResult ok() {
|
||||
return new SaResult(CODE_SUCCESS, "ok", null);
|
||||
}
|
||||
public static SaResult ok(String msg) {
|
||||
return new SaResult(CODE_SUCCESS, msg, null);
|
||||
}
|
||||
public static SaResult code(int code) {
|
||||
return new SaResult(code, null, null);
|
||||
}
|
||||
public static SaResult data(Object data) {
|
||||
return new SaResult(CODE_SUCCESS, "ok", data);
|
||||
}
|
||||
|
||||
// 构建失败
|
||||
public static SaResult error() {
|
||||
return new SaResult(CODE_ERROR, "error", null);
|
||||
}
|
||||
public static SaResult error(String msg) {
|
||||
return new SaResult(CODE_ERROR, msg, null);
|
||||
}
|
||||
|
||||
// 构建指定状态码
|
||||
public static SaResult get(int code, String msg, Object data) {
|
||||
return new SaResult(code, msg, data);
|
||||
}
|
||||
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{"
|
||||
+ "\"code\": " + this.getCode()
|
||||
+ ", \"msg\": \"" + this.getMsg() + "\""
|
||||
+ ", \"data\": \"" + this.getData() + "\""
|
||||
+ "}";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,7 +13,7 @@ public class SaTokenConsts {
|
||||
/**
|
||||
* Sa-Token 当前版本号
|
||||
*/
|
||||
public static final String VERSION_NO = "v1.21.0";
|
||||
public static final String VERSION_NO = "v1.26.0";
|
||||
|
||||
/**
|
||||
* Sa-Token 开源地址
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
|
||||
<!-- 定义sa-token版本号 -->
|
||||
<properties>
|
||||
<sa-token-version>1.21.0</sa-token-version>
|
||||
<sa-token-version>1.26.0</sa-token-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- springboot依赖 -->
|
||||
<!-- SpringBoot依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
@@ -38,14 +38,14 @@
|
||||
<version>${sa-token-version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- sa-token整合redis (使用jackson序列化方式) -->
|
||||
<!-- Sa-Token 整合Redis (使用jackson序列化方式) -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-dao-redis-jackson</artifactId>
|
||||
<version>${sa-token-version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 提供redis连接池 -->
|
||||
<!-- 提供Redis连接池 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
|
||||
@@ -15,13 +15,13 @@ sa-token:
|
||||
# Redis数据库索引(默认为0)
|
||||
database: 2
|
||||
# Redis服务器地址
|
||||
host: 127.0.0.1
|
||||
host: 49.235.117.153
|
||||
# Redis服务器连接端口
|
||||
port: 6379
|
||||
# Redis服务器连接密码(默认为空)
|
||||
password:
|
||||
password: Kdfsjia.d.1212dsa
|
||||
# 连接超时时间(毫秒)
|
||||
timeout: 10ms
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
pool:
|
||||
# 连接池最大连接数
|
||||
@@ -45,7 +45,7 @@ spring:
|
||||
# Redis服务器连接密码(默认为空)
|
||||
password:
|
||||
# 连接超时时间(毫秒)
|
||||
timeout: 10ms
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
pool:
|
||||
# 连接池最大连接数
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义sa-token版本号 -->
|
||||
<properties>
|
||||
<sa-token-version>1.21.0</sa-token-version>
|
||||
<sa-token-version>1.26.0</sa-token-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
+4
-1
@@ -110,7 +110,10 @@ public class SaTokenJwtUtil {
|
||||
warn += "-------------------------------------";
|
||||
System.err.println(warn);
|
||||
}
|
||||
|
||||
|
||||
// 提前调用一下方法,促使其属性初始化
|
||||
StpUtil.getLoginType();
|
||||
|
||||
// 修改默认实现
|
||||
StpUtil.stpLogic = new StpLogic("login") {
|
||||
|
||||
|
||||
@@ -17,31 +17,44 @@
|
||||
<java.version>1.8</java.version>
|
||||
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
|
||||
<!-- 定义sa-token版本号 -->
|
||||
<sa-token-version>1.15.0.RELEASE</sa-token-version>
|
||||
<sa-token-version>1.26.0</sa-token-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- springboot依赖 -->
|
||||
<!-- SpringBoot依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- sa-token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
|
||||
<!-- 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>
|
||||
|
||||
<!-- thymeleaf 视图引擎 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OkHttps网络请求库: http://okhttps.ejlchina.com/ -->
|
||||
<dependency>
|
||||
<groupId>com.ejlchina</groupId>
|
||||
<artifactId>okhttps</artifactId>
|
||||
<version>2.4.5</version>
|
||||
<groupId>com.ejlchina</groupId>
|
||||
<artifactId>okhttps</artifactId>
|
||||
<version>3.1.1</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- 热刷新 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- ConfigurationProperties -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
+9
-2
@@ -4,7 +4,7 @@ import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 启动
|
||||
* 启动:Sa-OAuth2 ClientServer端
|
||||
* @author kong
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@@ -12,7 +12,14 @@ public class SaOAuth2ClientApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SaOAuth2ClientApplication.class, args);
|
||||
System.out.println("\n客户端启动成功,访问: http://localhost:8002/login.html");
|
||||
System.out.println("\nSa-Token-OAuth Client端启动成功\n\n" + str);
|
||||
}
|
||||
|
||||
static String str = "-------------------- Sa-Token-OAuth2 示例 --------------------\n\n" +
|
||||
"首先在host文件 (C:\\windows\\system32\\drivers\\etc\\hosts) 添加以下内容: \r\n" +
|
||||
" 127.0.0.1 sa-oauth-server.com \r\n" +
|
||||
" 127.0.0.1 sa-oauth-client.com \r\n" +
|
||||
"再从浏览器访问:\r\n" +
|
||||
" http://sa-oauth-client.com:8002";
|
||||
|
||||
}
|
||||
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
package com.pj.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.ejlchina.okhttps.OkHttps;
|
||||
import com.pj.utils.AjaxJson;
|
||||
import com.pj.utils.SoMap;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
|
||||
/**
|
||||
* 登录注册Controller
|
||||
* @author kong
|
||||
*/
|
||||
@RestController
|
||||
public class ClientAccController {
|
||||
|
||||
|
||||
// 返回当前登录者的账号id, 如果未登录, 返回null
|
||||
@RequestMapping("/getLoginInfo")
|
||||
public AjaxJson getLoginInfo() {
|
||||
Object loginId = StpUtil.getLoginIdDefaultNull();
|
||||
return AjaxJson.getSuccessData(loginId);
|
||||
}
|
||||
|
||||
// 注销登录
|
||||
@RequestMapping("/logout")
|
||||
public AjaxJson logout() {
|
||||
StpUtil.logout();
|
||||
return AjaxJson.getSuccess();
|
||||
}
|
||||
|
||||
// 根据code码进行登录
|
||||
@RequestMapping("/doCodeLogin")
|
||||
public AjaxJson doCodeLogin(String code) {
|
||||
System.out.println("------------------ 成功进入请求 ------------------");
|
||||
|
||||
// 请求服务提供方接口地址,获取 access_token 以及其他信息
|
||||
// 携带三个关键参数: code、client_id、client_secret
|
||||
String str = OkHttps.sync("http://localhost:8001/oauth2/getAccessToken")
|
||||
.addBodyPara("code", code)
|
||||
.addBodyPara("client_id", "123123123")
|
||||
.addBodyPara("client_secret", "aaaa-bbbb-cccc-dddd-eeee")
|
||||
.post()
|
||||
.getBody()
|
||||
.toString();
|
||||
SoMap so = SoMap.getSoMap().setJsonString(str);
|
||||
System.out.println("返回结果: " + so);
|
||||
|
||||
// code不等于200 代表请求失败
|
||||
if(so.getInt("code") != 200) {
|
||||
return AjaxJson.getError(so.getString("msg"));
|
||||
}
|
||||
|
||||
// 根据openid获取其对应的userId
|
||||
String openid = so.getString("openid");
|
||||
long userId = getUserIdByOpenid(openid);
|
||||
|
||||
// 登录并返回账号信息
|
||||
StpUtil.setLoginId(userId);
|
||||
return AjaxJson.getSuccessData(userId).set("openid", openid);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ------------ 模拟方法 ------------------
|
||||
|
||||
// 模拟方法:根据openid获取userId
|
||||
private long getUserIdByOpenid(String openid) {
|
||||
// 此方法仅做模拟,实际开发要根据具体业务逻辑来获取userId
|
||||
return 10001;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
package com.pj.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.pj.utils.AjaxJson;
|
||||
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.exception.NotPermissionException;
|
||||
import cn.dev33.satoken.exception.NotRoleException;
|
||||
|
||||
/**
|
||||
* 全局异常拦截
|
||||
* <p> @ControllerAdvice 可指定包前缀,例如:(basePackages = "com.pj.controller.admin")
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
@ControllerAdvice
|
||||
public class ExceptionHandle {
|
||||
|
||||
|
||||
/** 全局异常拦截 */
|
||||
@ResponseBody
|
||||
@ExceptionHandler
|
||||
public AjaxJson handlerException(Exception e) {
|
||||
|
||||
// 打印堆栈,以供调试
|
||||
e.printStackTrace();
|
||||
|
||||
// 记录日志信息
|
||||
AjaxJson aj = null;
|
||||
|
||||
// ------------- 判断异常类型,提供个性化提示信息
|
||||
|
||||
// 如果是未登录异常
|
||||
if(e instanceof NotLoginException){
|
||||
aj = AjaxJson.getNotLogin();
|
||||
}
|
||||
// 如果是角色异常
|
||||
else if(e instanceof NotRoleException) {
|
||||
NotPermissionException ee = (NotPermissionException) e;
|
||||
aj = AjaxJson.getNotJur("无此角色:" + ee.getCode());
|
||||
}
|
||||
// 如果是权限异常
|
||||
else if(e instanceof NotPermissionException) {
|
||||
NotPermissionException ee = (NotPermissionException) e;
|
||||
aj = AjaxJson.getNotJur("无此权限:" + ee.getCode());
|
||||
}
|
||||
// 普通异常输出:500 + 异常信息
|
||||
else {
|
||||
aj = AjaxJson.getError(e.getMessage());
|
||||
}
|
||||
|
||||
// 返回到前台
|
||||
return aj;
|
||||
}
|
||||
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
package com.pj.oauth2;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import com.ejlchina.okhttps.OkHttps;
|
||||
import com.pj.utils.SoMap;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
|
||||
/**
|
||||
* Sa-OAuth2 Client端 控制器
|
||||
* @author kong
|
||||
*/
|
||||
@RestController
|
||||
public class SaOAuthClientController {
|
||||
|
||||
// 相关参数配置
|
||||
private String clientId = "1001"; // 应用id
|
||||
private String clientSecret = "aaaa-bbbb-cccc-dddd-eeee"; // 应用秘钥
|
||||
private String serverUrl = "http://sa-oauth-server.com:8001"; // 服务端接口
|
||||
|
||||
// 进入首页
|
||||
@RequestMapping("/")
|
||||
public Object index(HttpServletRequest request) {
|
||||
request.setAttribute("uid", StpUtil.getLoginIdDefaultNull());
|
||||
return new ModelAndView("index.html");
|
||||
}
|
||||
|
||||
// 根据Code码进行登录,获取 Access-Token 和 openid
|
||||
@RequestMapping("/codeLogin")
|
||||
public SaResult codeLogin(String code) {
|
||||
// 调用Server端接口,获取 Access-Token 以及其他信息
|
||||
String str = OkHttps.sync(serverUrl + "/oauth2/token")
|
||||
.addBodyPara("grant_type", "authorization_code")
|
||||
.addBodyPara("code", code)
|
||||
.addBodyPara("client_id", clientId)
|
||||
.addBodyPara("client_secret", clientSecret)
|
||||
.post()
|
||||
.getBody()
|
||||
.toString();
|
||||
SoMap so = SoMap.getSoMap().setJsonString(str);
|
||||
System.out.println("返回结果: " + so);
|
||||
|
||||
// code不等于200 代表请求失败
|
||||
if(so.getInt("code") != 200) {
|
||||
return SaResult.error(so.getString("msg"));
|
||||
}
|
||||
|
||||
// 根据openid获取其对应的userId
|
||||
SoMap data = so.getMap("data");
|
||||
long uid = getUserIdByOpenid(data.getString("openid"));
|
||||
data.set("uid", uid);
|
||||
|
||||
// 返回相关参数
|
||||
StpUtil.login(uid);
|
||||
return SaResult.data(data);
|
||||
}
|
||||
|
||||
// 根据 Refresh-Token 去刷新 Access-Token
|
||||
@RequestMapping("/refresh")
|
||||
public SaResult refresh(String refreshToken) {
|
||||
// 调用Server端接口,通过 Refresh-Token 刷新出一个新的 Access-Token
|
||||
String str = OkHttps.sync(serverUrl + "/oauth2/refresh")
|
||||
.addBodyPara("grant_type", "refresh_token")
|
||||
.addBodyPara("client_id", clientId)
|
||||
.addBodyPara("client_secret", clientSecret)
|
||||
.addBodyPara("refresh_token", refreshToken)
|
||||
.post()
|
||||
.getBody()
|
||||
.toString();
|
||||
SoMap so = SoMap.getSoMap().setJsonString(str);
|
||||
System.out.println("返回结果: " + so);
|
||||
|
||||
// code不等于200 代表请求失败
|
||||
if(so.getInt("code") != 200) {
|
||||
return SaResult.error(so.getString("msg"));
|
||||
}
|
||||
|
||||
// 返回相关参数 (data=新的Access-Token )
|
||||
SoMap data = so.getMap("data");
|
||||
return SaResult.data(data);
|
||||
}
|
||||
|
||||
// 模式三:密码式-授权登录
|
||||
@RequestMapping("/passwordLogin")
|
||||
public SaResult passwordLogin(String username, String password) {
|
||||
// 模式三:密码式-授权登录
|
||||
String str = OkHttps.sync(serverUrl + "/oauth2/token")
|
||||
.addBodyPara("grant_type", "password")
|
||||
.addBodyPara("client_id", clientId)
|
||||
.addBodyPara("username", username)
|
||||
.addBodyPara("password", password)
|
||||
.post()
|
||||
.getBody()
|
||||
.toString();
|
||||
SoMap so = SoMap.getSoMap().setJsonString(str);
|
||||
System.out.println("返回结果: " + so);
|
||||
|
||||
// code不等于200 代表请求失败
|
||||
if(so.getInt("code") != 200) {
|
||||
return SaResult.error(so.getString("msg"));
|
||||
}
|
||||
|
||||
// 根据openid获取其对应的userId
|
||||
SoMap data = so.getMap("data");
|
||||
long uid = getUserIdByOpenid(data.getString("openid"));
|
||||
data.set("uid", uid);
|
||||
|
||||
// 返回相关参数
|
||||
StpUtil.login(uid);
|
||||
return SaResult.data(data);
|
||||
}
|
||||
|
||||
// 模式四:获取应用的 Client-Token
|
||||
@RequestMapping("/clientToken")
|
||||
public SaResult clientToken() {
|
||||
// 调用Server端接口
|
||||
String str = OkHttps.sync(serverUrl + "/oauth2/client_token")
|
||||
.addBodyPara("grant_type", "client_credentials")
|
||||
.addBodyPara("client_id", clientId)
|
||||
.addBodyPara("client_secret", clientSecret)
|
||||
.post()
|
||||
.getBody()
|
||||
.toString();
|
||||
SoMap so = SoMap.getSoMap().setJsonString(str);
|
||||
System.out.println("返回结果: " + so);
|
||||
|
||||
// code不等于200 代表请求失败
|
||||
if(so.getInt("code") != 200) {
|
||||
return SaResult.error(so.getString("msg"));
|
||||
}
|
||||
|
||||
// 返回相关参数 (data=新的Client-Token )
|
||||
SoMap data = so.getMap("data");
|
||||
return SaResult.data(data);
|
||||
}
|
||||
|
||||
// 注销登录
|
||||
@RequestMapping("/logout")
|
||||
public SaResult logout() {
|
||||
StpUtil.logout();
|
||||
return SaResult.ok();
|
||||
}
|
||||
|
||||
// 根据 Access-Token 置换相关的资源: 获取账号昵称、头像、性别等信息
|
||||
@RequestMapping("/getUserinfo")
|
||||
public SaResult getUserinfo(String accessToken) {
|
||||
// 调用Server端接口,查询开放的资源
|
||||
String str = OkHttps.sync(serverUrl + "/oauth2/userinfo")
|
||||
.addBodyPara("access_token", accessToken)
|
||||
.post()
|
||||
.getBody()
|
||||
.toString();
|
||||
SoMap so = SoMap.getSoMap().setJsonString(str);
|
||||
System.out.println("返回结果: " + so);
|
||||
|
||||
// code不等于200 代表请求失败
|
||||
if(so.getInt("code") != 200) {
|
||||
return SaResult.error(so.getString("msg"));
|
||||
}
|
||||
|
||||
// 返回相关参数 (data=获取到的资源 )
|
||||
SoMap data = so.getMap("data");
|
||||
return SaResult.data(data);
|
||||
}
|
||||
|
||||
// 全局异常拦截
|
||||
@ExceptionHandler
|
||||
public SaResult handlerException(Exception e) {
|
||||
e.printStackTrace();
|
||||
return SaResult.error(e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
// ------------ 模拟方法 ------------------
|
||||
// 模拟方法:根据openid获取userId
|
||||
private long getUserIdByOpenid(String openid) {
|
||||
// 此方法仅做模拟,实际开发要根据具体业务逻辑来获取userId
|
||||
return 10001;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
package com.pj.utils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* ajax请求返回Json格式数据的封装 <br>
|
||||
* 所有预留字段:<br>
|
||||
* code=状态码 <br>
|
||||
* msg=描述信息 <br>
|
||||
* data=携带对象 <br>
|
||||
* pageNo=当前页 <br>
|
||||
* pageSize=页大小 <br>
|
||||
* startIndex=起始索引 <br>
|
||||
* dataCount=数据总数 <br>
|
||||
* pageCount=分页总数 <br>
|
||||
* <p> 返回范例:</p>
|
||||
* <pre>
|
||||
{
|
||||
"code": 200, // 成功时=200, 失败时=500 msg=失败原因
|
||||
"msg": "ok",
|
||||
"data": {}
|
||||
}
|
||||
</pre>
|
||||
*/
|
||||
public class AjaxJson extends LinkedHashMap<String, Object> implements Serializable{
|
||||
|
||||
private static final long serialVersionUID = 1L; // 序列化版本号
|
||||
|
||||
public static final int CODE_SUCCESS = 200; // 成功状态码
|
||||
public static final int CODE_ERROR = 500; // 错误状态码
|
||||
public static final int CODE_WARNING = 501; // 警告状态码
|
||||
public static final int CODE_NOT_JUR = 403; // 无权限状态码
|
||||
public static final int CODE_NOT_LOGIN = 401; // 未登录状态码
|
||||
public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码
|
||||
|
||||
|
||||
|
||||
// ============================ 写值取值 ==================================
|
||||
|
||||
/** 给code赋值,连缀风格 */
|
||||
public AjaxJson setCode(int code) {
|
||||
this.put("code", code);
|
||||
return this;
|
||||
}
|
||||
/** 返回code */
|
||||
public Integer getCode() {
|
||||
return (Integer)this.get("code");
|
||||
}
|
||||
|
||||
/** 给msg赋值,连缀风格 */
|
||||
public AjaxJson setMsg(String msg) {
|
||||
this.put("msg", msg);
|
||||
return this;
|
||||
}
|
||||
/** 获取msg */
|
||||
public String getMsg() {
|
||||
return (String)this.get("msg");
|
||||
}
|
||||
|
||||
/** 给data赋值,连缀风格 */
|
||||
public AjaxJson setData(Object data) {
|
||||
this.put("data", data);
|
||||
return this;
|
||||
}
|
||||
/** 获取data */
|
||||
public String getData() {
|
||||
return (String)this.get("data");
|
||||
}
|
||||
/** 将data还原为指定类型并返回 */
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getData(Class<T> cs) {
|
||||
return (T) this.getData();
|
||||
}
|
||||
|
||||
/** 给dataCount(数据总数)赋值,连缀风格 */
|
||||
public AjaxJson setDataCount(Long dataCount) {
|
||||
this.put("dataCount", dataCount);
|
||||
// 如果提供了数据总数,则尝试计算page信息
|
||||
if(dataCount != null && dataCount >= 0) {
|
||||
// 如果:已有page信息
|
||||
if(get("pageNo") != null) {
|
||||
this.initPageInfo();
|
||||
}
|
||||
// // 或者:是JavaWeb环境
|
||||
// else if(SoMap.isJavaWeb()) {
|
||||
// SoMap so = SoMap.getRequestSoMap();
|
||||
// this.setPageNoAndSize(so.getKeyPageNo(), so.getKeyPageSize());
|
||||
// this.initPageInfo();
|
||||
// }
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/** 获取dataCount(数据总数) */
|
||||
public String getDataCount() {
|
||||
return (String)this.get("dataCount");
|
||||
}
|
||||
|
||||
/** 设置pageNo 和 pageSize,并计算出startIndex于pageCount */
|
||||
public AjaxJson setPageNoAndSize(long pageNo, long pageSize) {
|
||||
this.put("pageNo", pageNo);
|
||||
this.put("pageSize", pageSize);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 根据 pageSize dataCount,计算startIndex 与 pageCount */
|
||||
public AjaxJson initPageInfo() {
|
||||
long pageNo = (long)this.get("pageNo");
|
||||
long pageSize = (long)this.get("pageSize");
|
||||
long dataCount = (long)this.get("dataCount");
|
||||
this.set("startIndex", (pageNo - 1) * pageSize);
|
||||
long pc = dataCount / pageSize;
|
||||
this.set("pageCount", (dataCount % pageSize == 0 ? pc : pc + 1));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/** 写入一个值 自定义key, 连缀风格 */
|
||||
public AjaxJson set(String key, Object data) {
|
||||
this.put(key, data);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 写入一个Map, 连缀风格 */
|
||||
public AjaxJson setMap(Map<String, ?> map) {
|
||||
for (String key : map.keySet()) {
|
||||
this.put(key, map.get(key));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// ============================ 构建 ==================================
|
||||
|
||||
public AjaxJson(int code, String msg, Object data, Long dataCount) {
|
||||
this.setCode(code);
|
||||
this.setMsg(msg);
|
||||
this.setData(data);
|
||||
this.setDataCount(dataCount == null ? -1 : dataCount);
|
||||
}
|
||||
|
||||
/** 返回成功 */
|
||||
public static AjaxJson getSuccess() {
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", null, null);
|
||||
}
|
||||
public static AjaxJson getSuccess(String msg) {
|
||||
return new AjaxJson(CODE_SUCCESS, msg, null, null);
|
||||
}
|
||||
public static AjaxJson getSuccess(String msg, Object data) {
|
||||
return new AjaxJson(CODE_SUCCESS, msg, data, null);
|
||||
}
|
||||
public static AjaxJson getSuccessData(Object data) {
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
|
||||
}
|
||||
|
||||
|
||||
/** 返回失败 */
|
||||
public static AjaxJson getError() {
|
||||
return new AjaxJson(CODE_ERROR, "error", null, null);
|
||||
}
|
||||
public static AjaxJson getError(String msg) {
|
||||
return new AjaxJson(CODE_ERROR, msg, null, null);
|
||||
}
|
||||
|
||||
/** 返回警告 */
|
||||
public static AjaxJson getWarning() {
|
||||
return new AjaxJson(CODE_ERROR, "warning", null, null);
|
||||
}
|
||||
public static AjaxJson getWarning(String msg) {
|
||||
return new AjaxJson(CODE_WARNING, msg, null, null);
|
||||
}
|
||||
|
||||
/** 返回未登录 */
|
||||
public static AjaxJson getNotLogin() {
|
||||
return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
|
||||
}
|
||||
|
||||
/** 返回没有权限的 */
|
||||
public static AjaxJson getNotJur(String msg) {
|
||||
return new AjaxJson(CODE_NOT_JUR, msg, null, null);
|
||||
}
|
||||
|
||||
/** 返回一个自定义状态码的 */
|
||||
public static AjaxJson get(int code, String msg){
|
||||
return new AjaxJson(code, msg, null, null);
|
||||
}
|
||||
|
||||
/** 返回分页和数据的 */
|
||||
public static AjaxJson getPageData(Long dataCount, Object data){
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
|
||||
}
|
||||
|
||||
/** 返回, 根据受影响行数的(大于0=ok,小于0=error) */
|
||||
public static AjaxJson getByLine(int line){
|
||||
if(line > 0){
|
||||
return getSuccess("ok", line);
|
||||
}
|
||||
return getError("error").setData(line);
|
||||
}
|
||||
|
||||
/** 返回,根据布尔值来确定最终结果的 (true=ok,false=error) */
|
||||
public static AjaxJson getByBoolean(boolean b){
|
||||
return b ? getSuccess("ok") : getError("error");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// // 历史版本遗留代码
|
||||
// public int code; // 状态码
|
||||
// public String msg; // 描述信息
|
||||
// public Object data; // 携带对象
|
||||
// public Long dataCount; // 数据总数,用于分页
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -33,12 +33,10 @@ public class SoMap extends LinkedHashMap<String, Object> {
|
||||
public SoMap() {
|
||||
}
|
||||
|
||||
|
||||
/** 以下元素会在isNull函数中被判定为Null, */
|
||||
public static final Object[] NULL_ELEMENT_ARRAY = {null, ""};
|
||||
public static final List<Object> NULL_ELEMENT_LIST;
|
||||
|
||||
|
||||
static {
|
||||
NULL_ELEMENT_LIST = Arrays.asList(NULL_ELEMENT_ARRAY);
|
||||
}
|
||||
@@ -144,6 +142,22 @@ public class SoMap extends LinkedHashMap<String, Object> {
|
||||
return getDateByFormat(key, "yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/** 转为Map并返回 */
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public SoMap getMap(String key) {
|
||||
Object value = get(key);
|
||||
if(value == null) {
|
||||
return SoMap.getSoMap();
|
||||
}
|
||||
if(value instanceof Map) {
|
||||
return SoMap.getSoMap((Map)value);
|
||||
}
|
||||
if(value instanceof String) {
|
||||
return SoMap.getSoMap().setJsonString((String)value);
|
||||
}
|
||||
throw new RuntimeException("值无法转化为SoMap: " + value);
|
||||
}
|
||||
|
||||
/** 获取集合(必须原先就是个集合,否则会创建个新集合并返回) */
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<Object> getList(String key) {
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
server:
|
||||
port: 8002
|
||||
|
||||
spring:
|
||||
# 静态文件路径映射
|
||||
resources:
|
||||
static-locations: classpath:/META-INF/resources/,classpath:/resources/, classpath:/static/, classpath:/public/
|
||||
# static-locations: file:E:\work\project-yun\sa-token\sa-token-demo-oauth2\sa-token-demo-oauth2-client\src\main\resources\static\
|
||||
|
||||
# sa-token配置
|
||||
sa-token:
|
||||
# token名称 (同时也是cookie名称)
|
||||
token-name: satoken-client
|
||||
|
||||
# sa-token配置
|
||||
sa-token:
|
||||
# token名称 (同时也是cookie名称)
|
||||
token-name: satoken-client
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>客户端-登录页</title>
|
||||
<style type="text/css">
|
||||
*{margin: 0px; padding: 0px;}
|
||||
.login-box{width: 500px; margin: 50px auto;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>客户端-登录页</h2> <br>
|
||||
<div>
|
||||
当前是否登录: <span class="login-info"></span>
|
||||
<button onclick="logout()">注销登录</button>
|
||||
</div>
|
||||
<br/>
|
||||
点此按钮开始使用OAuth2.0开放平台快捷登录:
|
||||
<button onclick="requestOAuth2()">快捷登录</button>
|
||||
</div>
|
||||
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
// 获取登录信息
|
||||
var currUserId = null;
|
||||
function getLoginInfo() {
|
||||
$.ajax({
|
||||
url: '/getLoginInfo',
|
||||
data: {},
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if(res.data == null) {
|
||||
$(".login-info").html('<b style="color: red;">未登录</b>');
|
||||
// 如果当前未登录,并且此时url中有code参数, 则尝试使用code码进行登录
|
||||
var code = getParam('code', null);
|
||||
if(code != null) {
|
||||
doCodeLogin();
|
||||
}
|
||||
} else {
|
||||
$(".login-info").html('<b style="color: green;">已登录, userId=' + res.data + '</b>');
|
||||
currUserId = res.data;
|
||||
}
|
||||
},
|
||||
error: function(e, ee, eee) {
|
||||
console.log('error');
|
||||
alert(eee);
|
||||
}
|
||||
});
|
||||
}
|
||||
getLoginInfo();
|
||||
|
||||
// 注销登录
|
||||
function logout() {
|
||||
$.ajax({
|
||||
url: '/logout',
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
alert('注销成功');
|
||||
location.href = "./login.html";
|
||||
},
|
||||
error: function(e, ee, eee) {
|
||||
alert(eee);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 请求OAuth2授权
|
||||
function requestOAuth2() {
|
||||
// 如果当前已经登录,则必须先退出
|
||||
if(currUserId != null) {
|
||||
alert('当前已经登录, 请先注销');
|
||||
return;
|
||||
}
|
||||
// 拼接地址
|
||||
var url = "http://localhost:8001/oauth2/authorize" +
|
||||
"?client_id=123123123" + // 应用id
|
||||
"&scope=userinfo" + // 授权范围
|
||||
// "&redirect_uri=" + encodeURIComponent(location.href) // 重定向地址
|
||||
"&redirect_uri=" + encodeURIComponent("http://localhost:8002/login.html") // 重定向地址
|
||||
"&response_type=code" + // 返回格式
|
||||
"&state=123456789"; // 授权范围
|
||||
console.log(url);
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
// 使用code进行 (从url中获取code码进行登录)
|
||||
function doCodeLogin() {
|
||||
var code = getParam('code');
|
||||
$.ajax({
|
||||
url: '/doCodeLogin',
|
||||
data: {
|
||||
code: code
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if(res.code == 200) {
|
||||
alert('OAuth2登录成功');
|
||||
getLoginInfo(); // 刷新user信息
|
||||
} else {
|
||||
alert("登录失败:" + res.msg);
|
||||
}
|
||||
},
|
||||
error: function(e, ee, eee) {
|
||||
alert(eee);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 从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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,258 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Sa-OAuth2-Client端-测试页</title>
|
||||
<style type="text/css">
|
||||
body{background-color: #D0D9E0;}
|
||||
*{margin: 0px; padding: 0px;}
|
||||
.login-box{max-width: 1000px; margin: 30px auto; padding: 1em;}
|
||||
.info{line-height: 30px;}
|
||||
.btn-box{margin-top: 10px; margin-bottom: 15px;}
|
||||
.btn-box a{margin-right: 10px;}
|
||||
.btn-box a:hover{text-decoration:underline !important;}
|
||||
.login-box input{line-height: 25px; margin-bottom: 10px; padding-left: 5px;}
|
||||
.login-box button{padding: 5px 15px; margin-top: 20px; cursor: pointer; }
|
||||
.login-box a{text-decoration: none;}
|
||||
.pst{color: #666; margin-top: 15px;}
|
||||
.ps{color: #666; margin-left: 10px;}
|
||||
.login-box code{display: block; background-color: #F5F2F0; border: 1px #ccc solid; color: #600; padding: 15px; margin-top: 5px; border-radius: 2px; }
|
||||
.info b,.info span{color: green;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>Sa-OAuth2-Client端-测试页</h2> <br>
|
||||
<div class="info">
|
||||
<div>当前账号id:
|
||||
<b class="uid" th:utext="${uid}"></b>
|
||||
</div>
|
||||
<div>当前Openid: <span class="openid"></span></div>
|
||||
<div>当前Access-Token: <span class="access_token"></span></div>
|
||||
<div>当前Refresh-Token: <span class="refresh_token"></span></div>
|
||||
<div>当前令牌包含Scope: <span class="scope"></span></div>
|
||||
<div>当前Client-Token: <span class="client_token"></span></div>
|
||||
</div>
|
||||
<div class="btn-box">
|
||||
<a href="javascript:logout();">注销</a>
|
||||
<a href="/">回到首页</a>
|
||||
</div>
|
||||
<hr><br>
|
||||
|
||||
<h3>模式一:授权码(Authorization Code)</h3>
|
||||
<p class="pst">授权码:OAuth2.0标准授权流程,先 (重定向) 获取Code授权码,再 (Rest API) 获取 Access-Token 和 Openid </p>
|
||||
|
||||
<a href="http://sa-oauth-server.com:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/">
|
||||
<button>点我开始授权登录(静默授权)</button>
|
||||
</a>
|
||||
<span class="ps">当请求链接不包含scope权限时,将无需用户手动确认,做到静默授权,当然此时我们也只能获取openid</span>
|
||||
<code>http://sa-oauth-server.com:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/</code>
|
||||
|
||||
<a href="http://sa-oauth-server.com:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo">
|
||||
<button>授权登录(显式授权)</button>
|
||||
</a>
|
||||
<span class="ps">当请求链接包含具体的scope权限时,将需要用户手动确认,此时我们除了openid以外还可以获取更多的资源</span>
|
||||
<code>http://sa-oauth-server.com:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo</code>
|
||||
|
||||
<button onclick="refreshToken()">刷新令牌</button>
|
||||
<span class="ps">我们可以拿着 Refresh-Token 去刷新我们的 Access-Token,每次刷新后旧Token将作废</span>
|
||||
<code>http://sa-oauth-server.com:8001/oauth2/refresh?grant_type=refresh_token&client_id={value}&client_secret={value}&refresh_token={value}</code>
|
||||
|
||||
<button onclick="getUserinfo()">获取账号信息</button>
|
||||
<span class="ps">使用 Access-Token 置换资源: 获取账号昵称、头像、性别等信息 (Access-Token具备userinfo权限时才可以获取成功) </span>
|
||||
<code>http://sa-oauth-server.com:8001/oauth2/userinfo?access_token={value}</code>
|
||||
|
||||
<br>
|
||||
<h3>模式二:隐藏式(Implicit)</h3>
|
||||
<a href="http://sa-oauth-server.com:8001/oauth2/authorize?response_type=token&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo">
|
||||
<button>隐藏式</button>
|
||||
</a>
|
||||
<span class="ps">越过授权码的步骤,直接返回token到前端页面( 格式:http//:domain.com#token=xxxx-xxxx )</span>
|
||||
<code>http://sa-oauth-server.com:8001/oauth2/authorize?response_type=token&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo</code>
|
||||
|
||||
<br>
|
||||
<h3>模式三:密码式(Password)</h3>
|
||||
<p class="pst">在下面输入Server端的用户名和密码,使用密码式进行 OAuth2 授权登录</p>
|
||||
账号:<input name="username">
|
||||
密码:<input name="password">
|
||||
<button onclick="passwordLogin()">登录</button>
|
||||
<code>http://sa-oauth-server.com:8001/oauth2/token?grant_type=password&client_id={value}&username={value}&password={value}</code>
|
||||
|
||||
<br>
|
||||
<h3>模式四:凭证式(Client Credentials)</h3>
|
||||
<p class="pst">以上三种模式获取的都是用户的 Access-Token,代表用户对第三方应用的授权,在OAuth2.0中还有一种针对 Client级别的授权,
|
||||
即:Client-Token,代表应用自身的资源授权</p>
|
||||
<p class="pst">Client-Token具有延迟作废特性,即:在每次获取最新Client-Token的时候,旧Client-Token不会立即过期,而是作为Past-Token再次
|
||||
储存起来,资源请求方只要携带其中之一便可通过Token校验,这种特性保证了在大量并发请求时不会出现“新旧Token交替造成的授权失效”,
|
||||
保证了服务的高可用</p>
|
||||
|
||||
<button onclick="getClientToken()">获取应用Client-Token</button>
|
||||
<code>http://sa-oauth-server.com:8001/oauth2/client_token?grant_type=client_credentials&client_id={value}&client_secret={value}</code>
|
||||
|
||||
<br><br>
|
||||
<span>更多资料请参考 Sa-Token 官方文档地址:</span>
|
||||
<a href="http://sa-token.dev33.cn/">http://sa-token.dev33.cn/</a>
|
||||
|
||||
<div style="height: 200px;"></div>
|
||||
</div>
|
||||
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
|
||||
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
|
||||
<script type="text/javascript">
|
||||
|
||||
// 根据code授权码进行登录
|
||||
function doLogin(code) {
|
||||
$.ajax({
|
||||
url: '/codeLogin?code=' + code,
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
console.log('返回:', res);
|
||||
if(res.code == 200) {
|
||||
setInfo(res.data);
|
||||
layer.msg('登录成功!');
|
||||
} else {
|
||||
layer.msg(res.msg);
|
||||
}
|
||||
},
|
||||
error: function(xhr, type, errorThrown){
|
||||
return layer.alert("异常:" + JSON.stringify(xhr));
|
||||
}
|
||||
});
|
||||
}
|
||||
var code = getParam('code');
|
||||
if(code) {
|
||||
doLogin(code);
|
||||
}
|
||||
|
||||
// 根据 Refresh-Token 去刷新 Access-Token
|
||||
function refreshToken() {
|
||||
var refreshToken = $('.refresh_token').text();
|
||||
if(refreshToken == '') {
|
||||
return layer.alert('您还没有获取 Refresh-Token ,请先授权登录');
|
||||
}
|
||||
$.ajax({
|
||||
url: '/refresh?refreshToken=' + refreshToken,
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
console.log('返回:', res);
|
||||
if(res.code == 200) {
|
||||
setInfo(res.data);
|
||||
layer.msg('登录成功!');
|
||||
} else {
|
||||
layer.msg(res.msg);
|
||||
}
|
||||
},
|
||||
error: function(xhr, type, errorThrown){
|
||||
return layer.alert("异常:" + JSON.stringify(xhr));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 模式三:密码式-授权登录
|
||||
function passwordLogin() {
|
||||
$.ajax({
|
||||
url: '/passwordLogin',
|
||||
data: {
|
||||
username: $('[name=username]').val(),
|
||||
password: $('[name=password]').val()
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
console.log('返回:', res);
|
||||
if(res.code == 200) {
|
||||
setInfo(res.data);
|
||||
layer.msg('登录成功!');
|
||||
} else {
|
||||
layer.msg(res.msg);
|
||||
}
|
||||
},
|
||||
error: function(xhr, type, errorThrown){
|
||||
return layer.alert("异常:" + JSON.stringify(xhr));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 模式四:获取应用的 Client-Token
|
||||
function getClientToken () {
|
||||
$.ajax({
|
||||
url: '/clientToken',
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
console.log('返回:', res);
|
||||
if(res.code == 200) {
|
||||
setInfo(res.data);
|
||||
layer.msg('获取成功!');
|
||||
} else {
|
||||
layer.msg(res.msg);
|
||||
}
|
||||
},
|
||||
error: function(xhr, type, errorThrown){
|
||||
return layer.alert("异常:" + JSON.stringify(xhr));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 使用 Access-Token 置换资源: 获取账号昵称、头像、性别等信息
|
||||
function getUserinfo() {
|
||||
var accessToken = $('.access_token').text();
|
||||
if(accessToken == '') {
|
||||
return layer.alert('您还没有获取 Access-Token ,请先授权登录');
|
||||
}
|
||||
$.ajax({
|
||||
url: '/getUserinfo',
|
||||
data: {accessToken: accessToken},
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if(res.code == 200) {
|
||||
layer.alert(JSON.stringify(res.data));
|
||||
} else {
|
||||
layer.alert(res.msg);
|
||||
}
|
||||
},
|
||||
error: function(xhr, type, errorThrown){
|
||||
return layer.alert("异常:" + JSON.stringify(xhr));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 注销
|
||||
function logout() {
|
||||
$.ajax({
|
||||
url: '/logout',
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
location.href = '/';
|
||||
},
|
||||
error: function(xhr, type, errorThrown){
|
||||
return layer.alert("异常:" + JSON.stringify(xhr));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 写入数据
|
||||
function setInfo(info) {
|
||||
console.log('info', info);
|
||||
for (var key in info) {
|
||||
$('.' + key).text(info[key]);
|
||||
}
|
||||
if($('.uid').text() == '') {
|
||||
$('.uid').html('<b style="color: #E00;">未登录</b>')
|
||||
}
|
||||
}
|
||||
setInfo({});
|
||||
|
||||
// 从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);
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -17,33 +17,33 @@
|
||||
<java.version>1.8</java.version>
|
||||
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
|
||||
<!-- 定义sa-token版本号 -->
|
||||
<sa-token-version>1.15.0.RELEASE</sa-token-version>
|
||||
<sa-token-version>1.26.0</sa-token-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- springboot依赖 -->
|
||||
<!-- SpringBoot依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- sa-token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
|
||||
<!-- 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 实现 oauth2.0 -->
|
||||
<!-- Sa-Token-OAuth2.0 模块 -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-oauth2</artifactId>
|
||||
<version>1.15.0-alpha</version>
|
||||
<version>${sa-token-version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- sa-token整合redis (使用jackson序列化方式) -->
|
||||
<!-- <dependency>
|
||||
<!-- Sa-Token整合Redis (使用jackson序列化方式) -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-dao-redis-jackson</artifactId>
|
||||
<version>${sa-token-version}</version>
|
||||
@@ -51,7 +51,20 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency> -->
|
||||
</dependency>
|
||||
|
||||
<!-- thymeleaf 视图引擎 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 热刷新 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- ConfigurationProperties -->
|
||||
<dependency>
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 启动
|
||||
* 启动:Sa-OAuth2 Server端
|
||||
* @author kong
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@@ -12,7 +12,7 @@ public class SaOAuth2ServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SaOAuth2ServerApplication.class, args);
|
||||
System.out.println("\n服务端启动成功");
|
||||
System.out.println("\nSa-Token-OAuth Server端启动成功");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
package com.pj.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.pj.utils.AjaxJson;
|
||||
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.exception.NotPermissionException;
|
||||
import cn.dev33.satoken.exception.NotRoleException;
|
||||
|
||||
/**
|
||||
* 全局异常拦截
|
||||
* <p> @ControllerAdvice 可指定包前缀,例如:(basePackages = "com.pj.controller.admin")
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
@ControllerAdvice
|
||||
public class ExceptionHandle {
|
||||
|
||||
|
||||
/** 全局异常拦截 */
|
||||
@ResponseBody
|
||||
@ExceptionHandler
|
||||
public AjaxJson handlerException(Exception e) {
|
||||
|
||||
// 打印堆栈,以供调试
|
||||
e.printStackTrace();
|
||||
|
||||
// 记录日志信息
|
||||
AjaxJson aj = null;
|
||||
|
||||
// ------------- 判断异常类型,提供个性化提示信息
|
||||
|
||||
// 如果是未登录异常
|
||||
if(e instanceof NotLoginException){
|
||||
aj = AjaxJson.getNotLogin();
|
||||
}
|
||||
// 如果是角色异常
|
||||
else if(e instanceof NotRoleException) {
|
||||
NotPermissionException ee = (NotPermissionException) e;
|
||||
aj = AjaxJson.getNotJur("无此角色:" + ee.getCode());
|
||||
}
|
||||
// 如果是权限异常
|
||||
else if(e instanceof NotPermissionException) {
|
||||
NotPermissionException ee = (NotPermissionException) e;
|
||||
aj = AjaxJson.getNotJur("无此权限:" + ee.getCode());
|
||||
}
|
||||
// 普通异常输出:500 + 异常信息
|
||||
else {
|
||||
aj = AjaxJson.getError(e.getMessage());
|
||||
}
|
||||
|
||||
// 返回到前台
|
||||
return aj;
|
||||
}
|
||||
|
||||
}
|
||||
-147
@@ -1,147 +0,0 @@
|
||||
package com.pj.controller;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.pj.utils.AjaxJson;
|
||||
import com.pj.utils.SoMap;
|
||||
|
||||
import cn.dev33.satoken.oauth2.logic.SaOAuth2Util;
|
||||
import cn.dev33.satoken.oauth2.model.AccessTokenModel;
|
||||
import cn.dev33.satoken.oauth2.model.CodeModel;
|
||||
import cn.dev33.satoken.oauth2.model.RequestAuthModel;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/oauth2/")
|
||||
public class OAuth2Controller {
|
||||
|
||||
|
||||
// 获取授权码
|
||||
@RequestMapping("/authorize")
|
||||
public AjaxJson authorize(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
|
||||
// 获取参数
|
||||
System.out.println("------------------ 成功进入请求 ------------------");
|
||||
|
||||
// 如果暂未登录,则先跳转到登录页 (转发)
|
||||
if(StpUtil.isLogin() == false) {
|
||||
response.setContentType("text/html");
|
||||
request.getRequestDispatcher("/login.html").forward(request, response);
|
||||
return AjaxJson.getSuccess();
|
||||
}
|
||||
|
||||
// 构建Model
|
||||
RequestAuthModel authModel = new RequestAuthModel()
|
||||
.setClientId(request.getParameter("client_id")) // 应用id
|
||||
.setScope(request.getParameter("scope")) // 授权类型
|
||||
.setLoginId(StpUtil.getLoginIdAsLong()) // 当前登录账号id
|
||||
.setRedirectUri(URLDecoder.decode(request.getParameter("redirect_uri"), "utf-8")) // 重定向地址
|
||||
.setResponseType(request.getParameter("response_type")) // 返回类型
|
||||
.setState(request.getParameter("state")) // 状态值
|
||||
.checkModel(); // 校验参数完整性
|
||||
|
||||
// 生成授权码Model
|
||||
CodeModel codeModel = SaOAuth2Util.generateCode(authModel);
|
||||
|
||||
// 打印调试
|
||||
System.out.println("应用id=" + authModel.getClientId() + "请求授权,授权类型=" + authModel.getResponseType());
|
||||
System.out.println("重定向地址:" + authModel.getRedirectUri());
|
||||
System.out.println("拼接完成的redirect_uri: " + codeModel.getRedirectUri());
|
||||
System.out.println("如果用户拒绝授权,则重定向至: " + codeModel.getRejectUri());
|
||||
|
||||
// 如果请求的权限用户已经确认,直接开始重定向授权
|
||||
if(codeModel.getIsConfirm() == true) {
|
||||
response.sendRedirect(codeModel.getRedirectUri());
|
||||
} else {
|
||||
// 如果请求的权限用户尚未确认,则进入到确定页
|
||||
request.setAttribute("name", "sdd");
|
||||
response.sendRedirect("/auth.html?code=" + codeModel.getCode());
|
||||
}
|
||||
|
||||
return AjaxJson.getSuccess();
|
||||
}
|
||||
|
||||
// 根据授权码获取应用信息
|
||||
@RequestMapping("/getCodeInfo")
|
||||
public AjaxJson getCodeInfo(String code) {
|
||||
// 获取codeModel
|
||||
CodeModel codeModel = SaOAuth2Util.getCode(code);
|
||||
System.out.println(code);
|
||||
System.out.println(codeModel);
|
||||
// 返回
|
||||
return AjaxJson.getSuccessData(codeModel);
|
||||
}
|
||||
|
||||
// 确认授权一个授权码
|
||||
@RequestMapping("/confirm")
|
||||
public AjaxJson confirm(String code) {
|
||||
// 获取codeModel
|
||||
CodeModel codeModel = SaOAuth2Util.getCode(code);
|
||||
if(codeModel == null) {
|
||||
return AjaxJson.getError("无效code码");
|
||||
}
|
||||
// 此处的判断是为了保证当前账号id 和 创建授权码的账号id一致 才可以进行确认
|
||||
if(codeModel.getLoginId().toString().equals(StpUtil.getLoginIdAsString()) == false) {
|
||||
return AjaxJson.getError("暂无权限");
|
||||
}
|
||||
// 进行确认
|
||||
SaOAuth2Util.confirmCode(code);
|
||||
|
||||
// 返回ok
|
||||
return AjaxJson.getSuccess();
|
||||
}
|
||||
|
||||
// 根据授权码等参数,获取 access_token 等信息
|
||||
@RequestMapping("/getAccessToken")
|
||||
public SoMap getAccessToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
// 获取参数
|
||||
System.out.println("------------------ 成功进入请求 ------------------");
|
||||
String code = request.getParameter("code"); // code码
|
||||
String clientId = request.getParameter("client_id"); // 应用id
|
||||
String clientSecret = request.getParameter("client_secret"); // 应用秘钥
|
||||
|
||||
// 校验参数
|
||||
SaOAuth2Util.checkCodeIdSecret(code, clientId, clientSecret);
|
||||
|
||||
// 生成
|
||||
CodeModel codeModel = SaOAuth2Util.getCode(code);
|
||||
AccessTokenModel tokenModel = SaOAuth2Util.generateAccessToken(codeModel);
|
||||
|
||||
// 生成AccessToken之后,将授权码立即销毁
|
||||
SaOAuth2Util.deleteCode(code);
|
||||
|
||||
// 返回
|
||||
return SoMap.getSoMap()
|
||||
.setModel(tokenModel)
|
||||
.set("code", 200)
|
||||
.set("msg", "ok");
|
||||
}
|
||||
|
||||
// 根据access_token返回指定的资源
|
||||
@RequestMapping("/getResources")
|
||||
public SoMap getResources(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
|
||||
// 获取信息
|
||||
String accessToken = request.getParameter("access_token");
|
||||
Object LoginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken);
|
||||
System.out.println("LoginId=" + LoginId);
|
||||
|
||||
// 根据LoginId获取相应信息...
|
||||
// 此处仅做模拟
|
||||
return new SoMap()
|
||||
.set("nickname", "shengzhang")
|
||||
.set("acatar", "xxx")
|
||||
.set("sex", 1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
package com.pj.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.pj.utils.AjaxJson;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
|
||||
/**
|
||||
* 服务端登录Controller
|
||||
* @author kong
|
||||
*/
|
||||
@RestController
|
||||
public class ServerAccController {
|
||||
|
||||
// 登录方法
|
||||
@RequestMapping("/doLogin")
|
||||
public AjaxJson test(String username, String password) {
|
||||
System.out.println("------------------ 成功进入请求 ------------------");
|
||||
if("test".equals(username) && "test".equals(password)) {
|
||||
StpUtil.setLoginId(10001);
|
||||
return AjaxJson.getSuccess();
|
||||
}
|
||||
return AjaxJson.getError();
|
||||
}
|
||||
|
||||
}
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
package com.pj.oauth2;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import cn.dev33.satoken.oauth2.logic.SaOAuth2Interface;
|
||||
|
||||
/**
|
||||
* 使用oauth2.0 所必须的一些自定义实现
|
||||
* @author kong
|
||||
*/
|
||||
@Component
|
||||
public class SaOAuth2InterfaceImpl implements SaOAuth2Interface {
|
||||
|
||||
|
||||
/*
|
||||
* ------ 注意: 以下代码均为示例,真实环境需要根据数据库查询相关信息
|
||||
*/
|
||||
|
||||
// 返回此平台所有权限集合
|
||||
@Override
|
||||
public List<String> getAppScopeList() {
|
||||
return Arrays.asList("userinfo");
|
||||
}
|
||||
|
||||
// 返回指定Client签约的所有Scope集合
|
||||
@Override
|
||||
public List<String> getClientScopeList(String clientId) {
|
||||
return Arrays.asList("userinfo");
|
||||
}
|
||||
|
||||
// 获取指定 LoginId 对指定 Client 已经授权过的所有 Scope
|
||||
@Override
|
||||
public List<String> getGrantScopeList(Object loginId, String clientId) {
|
||||
return Arrays.asList();
|
||||
}
|
||||
|
||||
// 返回指定Client允许的回调域名, 多个用逗号隔开, *代表不限制
|
||||
@Override
|
||||
public String getClientDomain(String clientId) {
|
||||
return "*";
|
||||
}
|
||||
|
||||
// 返回指定ClientId的ClientSecret
|
||||
@Override
|
||||
public String getClientSecret(String clientId) {
|
||||
return "aaaa-bbbb-cccc-dddd-eeee";
|
||||
}
|
||||
|
||||
// 根据ClientId和LoginId返回openid
|
||||
@Override
|
||||
public String getOpenid(String clientId, Object loginId) {
|
||||
return "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__";
|
||||
}
|
||||
|
||||
// 根据ClientId和openid返回LoginId
|
||||
@Override
|
||||
public Object getLoginId(String clientId, String openid) {
|
||||
return 10001;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* 以上函数为开发时必须重写实现,其余函数可以按需重写
|
||||
*/
|
||||
|
||||
|
||||
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package com.pj.oauth2;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.oauth2.config.SaOAuth2Config;
|
||||
import cn.dev33.satoken.oauth2.logic.SaOAuth2Handle;
|
||||
import cn.dev33.satoken.oauth2.logic.SaOAuth2Util;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
|
||||
/**
|
||||
* Sa-OAuth2 Server端 控制器
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
@RestController
|
||||
public class SaOAuth2ServerController {
|
||||
|
||||
// 处理所有OAuth相关请求
|
||||
@RequestMapping("/oauth2/*")
|
||||
public Object request() {
|
||||
System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl());
|
||||
return SaOAuth2Handle.serverRequest();
|
||||
}
|
||||
|
||||
// Sa-OAuth2 定制化配置
|
||||
@Autowired
|
||||
public void setSaOAuth2Config(SaOAuth2Config cfg) {
|
||||
cfg.
|
||||
// 未登录的视图
|
||||
setNotLoginView(()->{
|
||||
return new ModelAndView("login.html");
|
||||
}).
|
||||
// 登录处理函数
|
||||
setDoLoginHandle((name, pwd) -> {
|
||||
if("sa".equals(name) && "123456".equals(pwd)) {
|
||||
StpUtil.login(10001);
|
||||
return SaResult.ok();
|
||||
}
|
||||
return SaResult.error("账号名或密码错误");
|
||||
}).
|
||||
// 授权确认视图
|
||||
setConfirmView((clientId, scope)->{
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("clientId", clientId);
|
||||
map.put("scope", scope);
|
||||
return new ModelAndView("confirm.html", map);
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
// 全局异常拦截
|
||||
@ExceptionHandler
|
||||
public SaResult handlerException(Exception e) {
|
||||
e.printStackTrace();
|
||||
return SaResult.error(e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
// ---------- 开放相关资源接口: Client端根据 Access-Token ,置换相关资源 ------------
|
||||
|
||||
// 获取Userinfo信息:昵称、头像、性别等等
|
||||
@RequestMapping("/oauth2/userinfo")
|
||||
public SaResult userinfo() {
|
||||
// 获取 Access-Token 对应的账号id
|
||||
String accessToken = SaHolder.getRequest().getParamNotNull("access_token");
|
||||
Object loginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken);
|
||||
System.out.println("-------- 此Access-Token对应的账号id: " + loginId);
|
||||
|
||||
// 校验 Access-Token 是否具有权限: userinfo
|
||||
SaOAuth2Util.checkScope(accessToken, "userinfo");
|
||||
|
||||
// 模拟账号信息 (真实环境需要查询数据库获取信息)
|
||||
Map<String, Object> map = new LinkedHashMap<String, Object>();
|
||||
map.put("nickname", "shengzhang_");
|
||||
map.put("avatar", "http://xxx.com/1.jpg");
|
||||
map.put("age", "18");
|
||||
map.put("sex", "男");
|
||||
map.put("address", "山东省 青岛市 城阳区");
|
||||
return SaResult.data(map);
|
||||
}
|
||||
|
||||
}
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
package com.pj.oauth2;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import cn.dev33.satoken.oauth2.SaOAuth2Manager;
|
||||
import cn.dev33.satoken.oauth2.config.SaOAuth2Config;
|
||||
import cn.dev33.satoken.oauth2.logic.SaOAuth2Interface;
|
||||
|
||||
/**
|
||||
* 利用Spring完成自动装配
|
||||
*
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public class SaOAuth2SpringAutowired {
|
||||
|
||||
/**
|
||||
* 获取OAuth2配置Bean
|
||||
*
|
||||
* @return 配置对象
|
||||
*/
|
||||
@Bean
|
||||
@ConfigurationProperties(prefix = "spring.sa-token.oauth2")
|
||||
public SaOAuth2Config getSaOAuth2Config() {
|
||||
return new SaOAuth2Config();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入OAuth2配置Bean
|
||||
*
|
||||
* @param saOAuth2Config 配置对象
|
||||
*/
|
||||
@Autowired
|
||||
public void setSaOAuth2Config(SaOAuth2Config saOAuth2Config) {
|
||||
SaOAuth2Manager.setConfig(saOAuth2Config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入OAuth2接口Bean
|
||||
*
|
||||
* @param saOAuth2Interface OAuth2接口Bean
|
||||
*/
|
||||
@Autowired(required = false)
|
||||
public void setSaOAuth2Interface(SaOAuth2Interface saOAuth2Interface) {
|
||||
SaOAuth2Manager.setInterface(saOAuth2Interface);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.pj.oauth2;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import cn.dev33.satoken.oauth2.logic.SaOAuth2Template;
|
||||
import cn.dev33.satoken.oauth2.model.SaClientModel;
|
||||
|
||||
/**
|
||||
* Sa-Token OAuth2.0 整合实现
|
||||
* @author kong
|
||||
*/
|
||||
@Component
|
||||
public class SaOAuth2TemplateImpl extends SaOAuth2Template {
|
||||
|
||||
// 根据 id 获取 Client 信息
|
||||
@Override
|
||||
public SaClientModel getClientModel(String clientId) {
|
||||
// 此为模拟数据,真实环境需要从数据库查询
|
||||
if("1001".equals(clientId)) {
|
||||
return new SaClientModel()
|
||||
.setClientId("10001")
|
||||
.setClientSecret("aaaa-bbbb-cccc-dddd-eeee")
|
||||
.setAllowUrl("*")
|
||||
.setContractScope("userinfo");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 根据ClientId 和 LoginId 获取openid
|
||||
@Override
|
||||
public String getOpenid(String clientId, Object loginId) {
|
||||
// 此为模拟数据,真实环境需要从数据库查询
|
||||
return "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__";
|
||||
}
|
||||
|
||||
// -------------- 其它需要重写的函数
|
||||
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
package com.pj.utils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* ajax请求返回Json格式数据的封装 <br>
|
||||
* 所有预留字段:<br>
|
||||
* code=状态码 <br>
|
||||
* msg=描述信息 <br>
|
||||
* data=携带对象 <br>
|
||||
* pageNo=当前页 <br>
|
||||
* pageSize=页大小 <br>
|
||||
* startIndex=起始索引 <br>
|
||||
* dataCount=数据总数 <br>
|
||||
* pageCount=分页总数 <br>
|
||||
* <p> 返回范例:</p>
|
||||
* <pre>
|
||||
{
|
||||
"code": 200, // 成功时=200, 失败时=500 msg=失败原因
|
||||
"msg": "ok",
|
||||
"data": {}
|
||||
}
|
||||
</pre>
|
||||
*/
|
||||
public class AjaxJson extends LinkedHashMap<String, Object> implements Serializable{
|
||||
|
||||
private static final long serialVersionUID = 1L; // 序列化版本号
|
||||
|
||||
public static final int CODE_SUCCESS = 200; // 成功状态码
|
||||
public static final int CODE_ERROR = 500; // 错误状态码
|
||||
public static final int CODE_WARNING = 501; // 警告状态码
|
||||
public static final int CODE_NOT_JUR = 403; // 无权限状态码
|
||||
public static final int CODE_NOT_LOGIN = 401; // 未登录状态码
|
||||
public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码
|
||||
|
||||
|
||||
|
||||
// ============================ 写值取值 ==================================
|
||||
|
||||
/** 给code赋值,连缀风格 */
|
||||
public AjaxJson setCode(int code) {
|
||||
this.put("code", code);
|
||||
return this;
|
||||
}
|
||||
/** 返回code */
|
||||
public Integer getCode() {
|
||||
return (Integer)this.get("code");
|
||||
}
|
||||
|
||||
/** 给msg赋值,连缀风格 */
|
||||
public AjaxJson setMsg(String msg) {
|
||||
this.put("msg", msg);
|
||||
return this;
|
||||
}
|
||||
/** 获取msg */
|
||||
public String getMsg() {
|
||||
return (String)this.get("msg");
|
||||
}
|
||||
|
||||
/** 给data赋值,连缀风格 */
|
||||
public AjaxJson setData(Object data) {
|
||||
this.put("data", data);
|
||||
return this;
|
||||
}
|
||||
/** 获取data */
|
||||
public String getData() {
|
||||
return (String)this.get("data");
|
||||
}
|
||||
/** 将data还原为指定类型并返回 */
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getData(Class<T> cs) {
|
||||
return (T) this.getData();
|
||||
}
|
||||
|
||||
/** 给dataCount(数据总数)赋值,连缀风格 */
|
||||
public AjaxJson setDataCount(Long dataCount) {
|
||||
this.put("dataCount", dataCount);
|
||||
// 如果提供了数据总数,则尝试计算page信息
|
||||
if(dataCount != null && dataCount >= 0) {
|
||||
// 如果:已有page信息
|
||||
if(get("pageNo") != null) {
|
||||
this.initPageInfo();
|
||||
}
|
||||
// // 或者:是JavaWeb环境
|
||||
// else if(SoMap.isJavaWeb()) {
|
||||
// SoMap so = SoMap.getRequestSoMap();
|
||||
// this.setPageNoAndSize(so.getKeyPageNo(), so.getKeyPageSize());
|
||||
// this.initPageInfo();
|
||||
// }
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/** 获取dataCount(数据总数) */
|
||||
public String getDataCount() {
|
||||
return (String)this.get("dataCount");
|
||||
}
|
||||
|
||||
/** 设置pageNo 和 pageSize,并计算出startIndex于pageCount */
|
||||
public AjaxJson setPageNoAndSize(long pageNo, long pageSize) {
|
||||
this.put("pageNo", pageNo);
|
||||
this.put("pageSize", pageSize);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 根据 pageSize dataCount,计算startIndex 与 pageCount */
|
||||
public AjaxJson initPageInfo() {
|
||||
long pageNo = (long)this.get("pageNo");
|
||||
long pageSize = (long)this.get("pageSize");
|
||||
long dataCount = (long)this.get("dataCount");
|
||||
this.set("startIndex", (pageNo - 1) * pageSize);
|
||||
long pc = dataCount / pageSize;
|
||||
this.set("pageCount", (dataCount % pageSize == 0 ? pc : pc + 1));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/** 写入一个值 自定义key, 连缀风格 */
|
||||
public AjaxJson set(String key, Object data) {
|
||||
this.put(key, data);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 写入一个Map, 连缀风格 */
|
||||
public AjaxJson setMap(Map<String, ?> map) {
|
||||
for (String key : map.keySet()) {
|
||||
this.put(key, map.get(key));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// ============================ 构建 ==================================
|
||||
|
||||
public AjaxJson(int code, String msg, Object data, Long dataCount) {
|
||||
this.setCode(code);
|
||||
this.setMsg(msg);
|
||||
this.setData(data);
|
||||
this.setDataCount(dataCount == null ? -1 : dataCount);
|
||||
}
|
||||
|
||||
/** 返回成功 */
|
||||
public static AjaxJson getSuccess() {
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", null, null);
|
||||
}
|
||||
public static AjaxJson getSuccess(String msg) {
|
||||
return new AjaxJson(CODE_SUCCESS, msg, null, null);
|
||||
}
|
||||
public static AjaxJson getSuccess(String msg, Object data) {
|
||||
return new AjaxJson(CODE_SUCCESS, msg, data, null);
|
||||
}
|
||||
public static AjaxJson getSuccessData(Object data) {
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
|
||||
}
|
||||
|
||||
|
||||
/** 返回失败 */
|
||||
public static AjaxJson getError() {
|
||||
return new AjaxJson(CODE_ERROR, "error", null, null);
|
||||
}
|
||||
public static AjaxJson getError(String msg) {
|
||||
return new AjaxJson(CODE_ERROR, msg, null, null);
|
||||
}
|
||||
|
||||
/** 返回警告 */
|
||||
public static AjaxJson getWarning() {
|
||||
return new AjaxJson(CODE_ERROR, "warning", null, null);
|
||||
}
|
||||
public static AjaxJson getWarning(String msg) {
|
||||
return new AjaxJson(CODE_WARNING, msg, null, null);
|
||||
}
|
||||
|
||||
/** 返回未登录 */
|
||||
public static AjaxJson getNotLogin() {
|
||||
return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
|
||||
}
|
||||
|
||||
/** 返回没有权限的 */
|
||||
public static AjaxJson getNotJur(String msg) {
|
||||
return new AjaxJson(CODE_NOT_JUR, msg, null, null);
|
||||
}
|
||||
|
||||
/** 返回一个自定义状态码的 */
|
||||
public static AjaxJson get(int code, String msg){
|
||||
return new AjaxJson(code, msg, null, null);
|
||||
}
|
||||
|
||||
/** 返回分页和数据的 */
|
||||
public static AjaxJson getPageData(Long dataCount, Object data){
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
|
||||
}
|
||||
|
||||
/** 返回, 根据受影响行数的(大于0=ok,小于0=error) */
|
||||
public static AjaxJson getByLine(int line){
|
||||
if(line > 0){
|
||||
return getSuccess("ok", line);
|
||||
}
|
||||
return getError("error").setData(line);
|
||||
}
|
||||
|
||||
/** 返回,根据布尔值来确定最终结果的 (true=ok,false=error) */
|
||||
public static AjaxJson getByBoolean(boolean b){
|
||||
return b ? getSuccess("ok") : getError("error");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// // 历史版本遗留代码
|
||||
// public int code; // 状态码
|
||||
// public String msg; // 描述信息
|
||||
// public Object data; // 携带对象
|
||||
// public Long dataCount; // 数据总数,用于分页
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,723 +0,0 @@
|
||||
package com.pj.utils;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Map< String, Object> 是最常用的一种Map类型,但是它写着麻烦
|
||||
* <p>所以特封装此类,继承Map,进行一些扩展,可以让Map更灵活使用
|
||||
* <p>最新:2020-12-10 新增部分构造方法
|
||||
* @author kong
|
||||
*/
|
||||
public class SoMap extends LinkedHashMap<String, Object> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public SoMap() {
|
||||
}
|
||||
|
||||
|
||||
/** 以下元素会在isNull函数中被判定为Null, */
|
||||
public static final Object[] NULL_ELEMENT_ARRAY = {null, ""};
|
||||
public static final List<Object> NULL_ELEMENT_LIST;
|
||||
|
||||
|
||||
static {
|
||||
NULL_ELEMENT_LIST = Arrays.asList(NULL_ELEMENT_ARRAY);
|
||||
}
|
||||
|
||||
// ============================= 读值 =============================
|
||||
|
||||
/** 获取一个值 */
|
||||
@Override
|
||||
public Object get(Object key) {
|
||||
if("this".equals(key)) {
|
||||
return this;
|
||||
}
|
||||
return super.get(key);
|
||||
}
|
||||
|
||||
/** 如果为空,则返回默认值 */
|
||||
public Object get(Object key, Object defaultValue) {
|
||||
Object value = get(key);
|
||||
if(valueIsNull(value)) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 转为String并返回 */
|
||||
public String getString(String key) {
|
||||
Object value = get(key);
|
||||
if(value == null) {
|
||||
return null;
|
||||
}
|
||||
return String.valueOf(value);
|
||||
}
|
||||
|
||||
/** 如果为空,则返回默认值 */
|
||||
public String getString(String key, String defaultValue) {
|
||||
Object value = get(key);
|
||||
if(valueIsNull(value)) {
|
||||
return defaultValue;
|
||||
}
|
||||
return String.valueOf(value);
|
||||
}
|
||||
|
||||
/** 转为int并返回 */
|
||||
public int getInt(String key) {
|
||||
Object value = get(key);
|
||||
if(valueIsNull(value)) {
|
||||
return 0;
|
||||
}
|
||||
return Integer.valueOf(String.valueOf(value));
|
||||
}
|
||||
/** 转为int并返回,同时指定默认值 */
|
||||
public int getInt(String key, int defaultValue) {
|
||||
Object value = get(key);
|
||||
if(valueIsNull(value)) {
|
||||
return defaultValue;
|
||||
}
|
||||
return Integer.valueOf(String.valueOf(value));
|
||||
}
|
||||
|
||||
/** 转为long并返回 */
|
||||
public long getLong(String key) {
|
||||
Object value = get(key);
|
||||
if(valueIsNull(value)) {
|
||||
return 0;
|
||||
}
|
||||
return Long.valueOf(String.valueOf(value));
|
||||
}
|
||||
|
||||
/** 转为double并返回 */
|
||||
public double getDouble(String key) {
|
||||
Object value = get(key);
|
||||
if(valueIsNull(value)) {
|
||||
return 0.0;
|
||||
}
|
||||
return Double.valueOf(String.valueOf(value));
|
||||
}
|
||||
|
||||
/** 转为boolean并返回 */
|
||||
public boolean getBoolean(String key) {
|
||||
Object value = get(key);
|
||||
if(valueIsNull(value)) {
|
||||
return false;
|
||||
}
|
||||
return Boolean.valueOf(String.valueOf(value));
|
||||
}
|
||||
|
||||
/** 转为Date并返回,根据自定义格式 */
|
||||
public Date getDateByFormat(String key, String format) {
|
||||
try {
|
||||
return new SimpleDateFormat(format).parse(getString(key));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** 转为Date并返回,根据格式: yyyy-MM-dd */
|
||||
public Date getDate(String key) {
|
||||
return getDateByFormat(key, "yyyy-MM-dd");
|
||||
}
|
||||
|
||||
/** 转为Date并返回,根据格式: yyyy-MM-dd HH:mm:ss */
|
||||
public Date getDateTime(String key) {
|
||||
return getDateByFormat(key, "yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/** 获取集合(必须原先就是个集合,否则会创建个新集合并返回) */
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<Object> getList(String key) {
|
||||
Object value = get(key);
|
||||
List<Object> list = null;
|
||||
if(value == null || value.equals("")) {
|
||||
list = new ArrayList<Object>();
|
||||
}
|
||||
else if(value instanceof List) {
|
||||
list = (List<Object>)value;
|
||||
} else {
|
||||
list = new ArrayList<Object>();
|
||||
list.add(value);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 获取集合 (指定泛型类型) */
|
||||
public <T> List<T> getList(String key, Class<T> cs) {
|
||||
List<Object> list = getList(key);
|
||||
List<T> list2 = new ArrayList<T>();
|
||||
for (Object obj : list) {
|
||||
T objC = getValueByClass(obj, cs);
|
||||
list2.add(objC);
|
||||
}
|
||||
return list2;
|
||||
}
|
||||
|
||||
/** 获取集合(逗号分隔式),(指定类型) */
|
||||
public <T> List<T> getListByComma(String key, Class<T> cs) {
|
||||
String listStr = getString(key);
|
||||
if(listStr == null || listStr.equals("")) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
// 开始转化
|
||||
String [] arr = listStr.split(",");
|
||||
List<T> list = new ArrayList<T>();
|
||||
for (String str : arr) {
|
||||
if(cs == int.class || cs == Integer.class || cs == long.class || cs == Long.class) {
|
||||
str = str.trim();
|
||||
}
|
||||
T objC = getValueByClass(str, cs);
|
||||
list.add(objC);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
/** 根据指定类型从map中取值,返回实体对象 */
|
||||
public <T> T getModel(Class<T> cs) {
|
||||
try {
|
||||
return getModelByObject(cs.newInstance());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** 从map中取值,塞到一个对象中 */
|
||||
public <T> T getModelByObject(T obj) {
|
||||
// 获取类型
|
||||
Class<?> cs = obj.getClass();
|
||||
// 循环复制
|
||||
for (Field field : cs.getDeclaredFields()) {
|
||||
try {
|
||||
// 获取对象
|
||||
Object value = this.get(field.getName());
|
||||
if(value == null) {
|
||||
continue;
|
||||
}
|
||||
field.setAccessible(true);
|
||||
Object valueConvert = getValueByClass(value, field.getType());
|
||||
field.set(obj, valueConvert);
|
||||
} catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
throw new RuntimeException("属性取值出错:" + field.getName(), e);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 将指定值转化为指定类型并返回
|
||||
* @param obj
|
||||
* @param cs
|
||||
* @param <T>
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T getValueByClass(Object obj, Class<T> cs) {
|
||||
String obj2 = String.valueOf(obj);
|
||||
Object obj3 = null;
|
||||
if (cs.equals(String.class)) {
|
||||
obj3 = obj2;
|
||||
} else if (cs.equals(int.class) || cs.equals(Integer.class)) {
|
||||
obj3 = Integer.valueOf(obj2);
|
||||
} else if (cs.equals(long.class) || cs.equals(Long.class)) {
|
||||
obj3 = Long.valueOf(obj2);
|
||||
} else if (cs.equals(short.class) || cs.equals(Short.class)) {
|
||||
obj3 = Short.valueOf(obj2);
|
||||
} else if (cs.equals(byte.class) || cs.equals(Byte.class)) {
|
||||
obj3 = Byte.valueOf(obj2);
|
||||
} else if (cs.equals(float.class) || cs.equals(Float.class)) {
|
||||
obj3 = Float.valueOf(obj2);
|
||||
} else if (cs.equals(double.class) || cs.equals(Double.class)) {
|
||||
obj3 = Double.valueOf(obj2);
|
||||
} else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) {
|
||||
obj3 = Boolean.valueOf(obj2);
|
||||
} else {
|
||||
obj3 = (T)obj;
|
||||
}
|
||||
return (T)obj3;
|
||||
}
|
||||
|
||||
|
||||
// ============================= 写值 =============================
|
||||
|
||||
/**
|
||||
* 给指定key添加一个默认值(只有在这个key原来无值的情况先才会set进去)
|
||||
*/
|
||||
public void setDefaultValue(String key, Object defaultValue) {
|
||||
if(isNull(key)) {
|
||||
set(key, defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
/** set一个值,连缀风格 */
|
||||
public SoMap set(String key, Object value) {
|
||||
// 防止敏感key
|
||||
if(key.toLowerCase().equals("this")) {
|
||||
return this;
|
||||
}
|
||||
put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 将一个Map塞进SoMap */
|
||||
public SoMap setMap(Map<String, ?> map) {
|
||||
if(map != null) {
|
||||
for (String key : map.keySet()) {
|
||||
this.set(key, map.get(key));
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 将一个对象解析塞进SoMap */
|
||||
public SoMap setModel(Object model) {
|
||||
if(model == null) {
|
||||
return this;
|
||||
}
|
||||
Field[] fields = model.getClass().getDeclaredFields();
|
||||
for (Field field : fields) {
|
||||
try{
|
||||
field.setAccessible(true);
|
||||
boolean isStatic = Modifier.isStatic(field.getModifiers());
|
||||
if(!isStatic) {
|
||||
this.set(field.getName(), field.get(model));
|
||||
}
|
||||
}catch (Exception e){
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 将json字符串解析后塞进SoMap */
|
||||
public SoMap setJsonString(String jsonString) {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = new ObjectMapper().readValue(jsonString, Map.class);
|
||||
return this.setMap(map);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================= 删值 =============================
|
||||
|
||||
/** delete一个值,连缀风格 */
|
||||
public SoMap delete(String key) {
|
||||
remove(key);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 清理所有value为null的字段 */
|
||||
public SoMap clearNull() {
|
||||
Iterator<String> iterator = this.keySet().iterator();
|
||||
while(iterator.hasNext()) {
|
||||
String key = iterator.next();
|
||||
if(this.isNull(key)) {
|
||||
iterator.remove();
|
||||
this.remove(key);
|
||||
}
|
||||
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/** 清理指定key */
|
||||
public SoMap clearIn(String ...keys) {
|
||||
List<String> keys2 = Arrays.asList(keys);
|
||||
Iterator<String> iterator = this.keySet().iterator();
|
||||
while(iterator.hasNext()) {
|
||||
String key = iterator.next();
|
||||
if(keys2.contains(key) == true) {
|
||||
iterator.remove();
|
||||
this.remove(key);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/** 清理掉不在列表中的key */
|
||||
public SoMap clearNotIn(String ...keys) {
|
||||
List<String> keys2 = Arrays.asList(keys);
|
||||
Iterator<String> iterator = this.keySet().iterator();
|
||||
while(iterator.hasNext()) {
|
||||
String key = iterator.next();
|
||||
if(keys2.contains(key) == false) {
|
||||
iterator.remove();
|
||||
this.remove(key);
|
||||
}
|
||||
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/** 清理掉所有key */
|
||||
public SoMap clearAll() {
|
||||
clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// ============================= 快速构建 =============================
|
||||
|
||||
/** 构建一个SoMap并返回 */
|
||||
public static SoMap getSoMap() {
|
||||
return new SoMap();
|
||||
}
|
||||
/** 构建一个SoMap并返回 */
|
||||
public static SoMap getSoMap(String key, Object value) {
|
||||
return new SoMap().set(key, value);
|
||||
}
|
||||
/** 构建一个SoMap并返回 */
|
||||
public static SoMap getSoMap(Map<String, ?> map) {
|
||||
return new SoMap().setMap(map);
|
||||
}
|
||||
|
||||
/** 将一个对象集合解析成为SoMap */
|
||||
public static SoMap getSoMapByModel(Object model) {
|
||||
return SoMap.getSoMap().setModel(model);
|
||||
}
|
||||
|
||||
/** 将一个对象集合解析成为SoMap集合 */
|
||||
public static List<SoMap> getSoMapByList(List<?> list) {
|
||||
List<SoMap> listMap = new ArrayList<SoMap>();
|
||||
for (Object model : list) {
|
||||
listMap.add(getSoMapByModel(model));
|
||||
}
|
||||
return listMap;
|
||||
}
|
||||
|
||||
/** 克隆指定key,返回一个新的SoMap */
|
||||
public SoMap cloneKeys(String... keys) {
|
||||
SoMap so = new SoMap();
|
||||
for (String key : keys) {
|
||||
so.set(key, this.get(key));
|
||||
}
|
||||
return so;
|
||||
}
|
||||
/** 克隆所有key,返回一个新的SoMap */
|
||||
public SoMap cloneSoMap() {
|
||||
SoMap so = new SoMap();
|
||||
for (String key : this.keySet()) {
|
||||
so.set(key, this.get(key));
|
||||
}
|
||||
return so;
|
||||
}
|
||||
|
||||
/** 将所有key转为大写 */
|
||||
public SoMap toUpperCase() {
|
||||
SoMap so = new SoMap();
|
||||
for (String key : this.keySet()) {
|
||||
so.set(key.toUpperCase(), this.get(key));
|
||||
}
|
||||
this.clearAll().setMap(so);
|
||||
return this;
|
||||
}
|
||||
/** 将所有key转为小写 */
|
||||
public SoMap toLowerCase() {
|
||||
SoMap so = new SoMap();
|
||||
for (String key : this.keySet()) {
|
||||
so.set(key.toLowerCase(), this.get(key));
|
||||
}
|
||||
this.clearAll().setMap(so);
|
||||
return this;
|
||||
}
|
||||
/** 将所有key中下划线转为中划线模式 (kebab-case风格) */
|
||||
public SoMap toKebabCase() {
|
||||
SoMap so = new SoMap();
|
||||
for (String key : this.keySet()) {
|
||||
so.set(wordEachKebabCase(key), this.get(key));
|
||||
}
|
||||
this.clearAll().setMap(so);
|
||||
return this;
|
||||
}
|
||||
/** 将所有key中下划线转为小驼峰模式 */
|
||||
public SoMap toHumpCase() {
|
||||
SoMap so = new SoMap();
|
||||
for (String key : this.keySet()) {
|
||||
so.set(wordEachBigFs(key), this.get(key));
|
||||
}
|
||||
this.clearAll().setMap(so);
|
||||
return this;
|
||||
}
|
||||
/** 将所有key中小驼峰转为下划线模式 */
|
||||
public SoMap humpToLineCase() {
|
||||
SoMap so = new SoMap();
|
||||
for (String key : this.keySet()) {
|
||||
so.set(wordHumpToLine(key), this.get(key));
|
||||
}
|
||||
this.clearAll().setMap(so);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ============================= 辅助方法 =============================
|
||||
|
||||
|
||||
/** 指定key是否为null,判定标准为 NULL_ELEMENT_ARRAY 中的元素 */
|
||||
public boolean isNull(String key) {
|
||||
return valueIsNull(get(key));
|
||||
}
|
||||
|
||||
/** 指定key列表中是否包含value为null的元素,只要有一个为null,就会返回true */
|
||||
public boolean isContainNull(String ...keys) {
|
||||
for (String key : keys) {
|
||||
if(this.isNull(key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 与isNull()相反 */
|
||||
public boolean isNotNull(String key) {
|
||||
return !isNull(key);
|
||||
}
|
||||
/** 指定key的value是否为null,作用同isNotNull() */
|
||||
public boolean has(String key) {
|
||||
return !isNull(key);
|
||||
}
|
||||
|
||||
/** 指定value在此SoMap的判断标准中是否为null */
|
||||
public boolean valueIsNull(Object value) {
|
||||
return NULL_ELEMENT_LIST.contains(value);
|
||||
}
|
||||
|
||||
/** 验证指定key不为空,为空则抛出异常 */
|
||||
public SoMap checkNull(String ...keys) {
|
||||
for (String key : keys) {
|
||||
if(this.isNull(key)) {
|
||||
throw new RuntimeException("参数" + key + "不能为空");
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
static Pattern patternNumber = Pattern.compile("[0-9]*");
|
||||
/** 指定key是否为数字 */
|
||||
public boolean isNumber(String key) {
|
||||
String value = getString(key);
|
||||
if(value == null) {
|
||||
return false;
|
||||
}
|
||||
return patternNumber.matcher(value).matches();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 转为JSON字符串
|
||||
*/
|
||||
public String toJsonString() {
|
||||
try {
|
||||
// SoMap so = SoMap.getSoMap(this);
|
||||
return new ObjectMapper().writeValueAsString(this);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 转为JSON字符串, 带格式的
|
||||
// */
|
||||
// public String toJsonFormatString() {
|
||||
// try {
|
||||
// return JSON.toJSONString(this, true);
|
||||
// } catch (Exception e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// }
|
||||
|
||||
// ============================= web辅助 =============================
|
||||
|
||||
|
||||
/**
|
||||
* 返回当前request请求的的所有参数
|
||||
* @return
|
||||
*/
|
||||
public static SoMap getRequestSoMap() {
|
||||
// 大善人SpringMVC提供的封装
|
||||
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if(servletRequestAttributes == null) {
|
||||
throw new RuntimeException("当前线程非JavaWeb环境");
|
||||
}
|
||||
// 当前request
|
||||
HttpServletRequest request = servletRequestAttributes.getRequest();
|
||||
if (request.getAttribute("currentSoMap") == null || request.getAttribute("currentSoMap") instanceof SoMap == false ) {
|
||||
initRequestSoMap(request);
|
||||
}
|
||||
return (SoMap)request.getAttribute("currentSoMap");
|
||||
}
|
||||
|
||||
/** 初始化当前request的 SoMap */
|
||||
private static void initRequestSoMap(HttpServletRequest request) {
|
||||
SoMap soMap = new SoMap();
|
||||
Map<String, String[]> parameterMap = request.getParameterMap(); // 获取所有参数
|
||||
for (String key : parameterMap.keySet()) {
|
||||
try {
|
||||
String[] values = parameterMap.get(key); // 获得values
|
||||
if(values.length == 1) {
|
||||
soMap.set(key, values[0]);
|
||||
} else {
|
||||
List<String> list = new ArrayList<String>();
|
||||
for (String v : values) {
|
||||
list.add(v);
|
||||
}
|
||||
soMap.set(key, list);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
request.setAttribute("currentSoMap", soMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证返回当前线程是否为JavaWeb环境
|
||||
* @return
|
||||
*/
|
||||
public static boolean isJavaWeb() {
|
||||
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// 大善人SpringMVC提供的封装
|
||||
if(servletRequestAttributes == null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============================= 常见key (以下key经常用,所以封装以下,方便写代码) =============================
|
||||
|
||||
/** get 当前页 */
|
||||
public int getKeyPageNo() {
|
||||
int pageNo = getInt("pageNo", 1);
|
||||
if(pageNo <= 0) {
|
||||
pageNo = 1;
|
||||
}
|
||||
return pageNo;
|
||||
}
|
||||
/** get 页大小 */
|
||||
public int getKeyPageSize() {
|
||||
int pageSize = getInt("pageSize", 10);
|
||||
if(pageSize <= 0 || pageSize > 1000) {
|
||||
pageSize = 10;
|
||||
}
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
/** get 排序方式 */
|
||||
public int getKeySortType() {
|
||||
return getInt("sortType");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ============================= 工具方法 =============================
|
||||
|
||||
|
||||
/**
|
||||
* 将一个一维集合转换为树形集合
|
||||
* @param list 集合
|
||||
* @param idKey id标识key
|
||||
* @param parentIdKey 父id标识key
|
||||
* @param childListKey 子节点标识key
|
||||
* @return 转换后的tree集合
|
||||
*/
|
||||
public static List<SoMap> listToTree(List<SoMap> list, String idKey, String parentIdKey, String childListKey) {
|
||||
// 声明新的集合,存储tree形数据
|
||||
List<SoMap> newTreeList = new ArrayList<SoMap>();
|
||||
// 声明hash-Map,方便查找数据
|
||||
SoMap hash = new SoMap();
|
||||
// 将数组转为Object的形式,key为数组中的id
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
SoMap json = (SoMap) list.get(i);
|
||||
hash.put(json.getString(idKey), json);
|
||||
}
|
||||
// 遍历结果集
|
||||
for (int j = 0; j < list.size(); j++) {
|
||||
// 单条记录
|
||||
SoMap aVal = (SoMap) list.get(j);
|
||||
// 在hash中取出key为单条记录中pid的值
|
||||
SoMap hashVp = (SoMap) hash.get(aVal.get(parentIdKey, "").toString());
|
||||
// 如果记录的pid存在,则说明它有父节点,将她添加到孩子节点的集合中
|
||||
if (hashVp != null) {
|
||||
// 检查是否有child属性,有则添加,没有则新建
|
||||
if (hashVp.get(childListKey) != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<SoMap> ch = (List<SoMap>) hashVp.get(childListKey);
|
||||
ch.add(aVal);
|
||||
hashVp.put(childListKey, ch);
|
||||
} else {
|
||||
List<SoMap> ch = new ArrayList<SoMap>();
|
||||
ch.add(aVal);
|
||||
hashVp.put(childListKey, ch);
|
||||
}
|
||||
} else {
|
||||
newTreeList.add(aVal);
|
||||
}
|
||||
}
|
||||
return newTreeList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 指定字符串的字符串下划线转大写模式 */
|
||||
private static String wordEachBig(String str){
|
||||
String newStr = "";
|
||||
for (String s : str.split("_")) {
|
||||
newStr += wordFirstBig(s);
|
||||
}
|
||||
return newStr;
|
||||
}
|
||||
/** 返回下划线转小驼峰形式 */
|
||||
private static String wordEachBigFs(String str){
|
||||
return wordFirstSmall(wordEachBig(str));
|
||||
}
|
||||
|
||||
/** 将指定单词首字母大写 */
|
||||
private static String wordFirstBig(String str) {
|
||||
return str.substring(0, 1).toUpperCase() + str.substring(1, str.length());
|
||||
}
|
||||
|
||||
/** 将指定单词首字母小写 */
|
||||
private static String wordFirstSmall(String str) {
|
||||
return str.substring(0, 1).toLowerCase() + str.substring(1, str.length());
|
||||
}
|
||||
|
||||
/** 下划线转中划线 */
|
||||
private static String wordEachKebabCase(String str) {
|
||||
return str.replaceAll("_", "-");
|
||||
}
|
||||
|
||||
/** 驼峰转下划线 */
|
||||
private static String wordHumpToLine(String str) {
|
||||
return str.replaceAll("[A-Z]", "_$0").toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
server:
|
||||
port: 8001
|
||||
|
||||
# sa-token配置
|
||||
sa-token:
|
||||
# token名称 (同时也是cookie名称)
|
||||
token-name: satoken-server
|
||||
# OAuth2.0 配置
|
||||
oauth2:
|
||||
is-code: true
|
||||
is-implicit: true
|
||||
is-password: true
|
||||
is-client: true
|
||||
|
||||
spring:
|
||||
# 静态文件路径映射
|
||||
resources:
|
||||
static-locations: classpath:/META-INF/resources/,classpath:/resources/, classpath:/static/, classpath:/public/
|
||||
# static-locations: file:E:\work\project-yun\sa-token\sa-token-demo-oauth2\sa-token-demo-oauth2-server\src\main\resources\static\
|
||||
|
||||
# sa-token配置
|
||||
sa-token:
|
||||
# token名称 (同时也是cookie名称)
|
||||
token-name: satoken-server
|
||||
|
||||
|
||||
# redis配置
|
||||
redis:
|
||||
# Redis数据库索引(默认为0)
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>服务提供方-确认授权页</title>
|
||||
<style type="text/css">
|
||||
*{margin: 0px; padding: 0px;}
|
||||
.login-box{width: 300px; margin: 50px auto; padding: 100px; border: 1px #000 solid;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>服务提供方-确认授权页</h2> <br>
|
||||
<div>
|
||||
<div>应用id: <span class="client_id"></span></div>
|
||||
<div>请求授权: <span class="scope"></span></div>
|
||||
<br>
|
||||
<div>是否同意授权: </div>
|
||||
<div>
|
||||
<button onclick="ok()">同意</button>
|
||||
<button onclick="no()">拒绝</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
// 获取code详细信息
|
||||
var code = getParam("code", null);
|
||||
var codeObj = null;
|
||||
if(code == null) {
|
||||
throw alert("无效code");
|
||||
}
|
||||
$.ajax({
|
||||
url: '/oauth2/getCodeInfo',
|
||||
data: {code: code},
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if(res.data == null) {
|
||||
return alert('无效授权码');
|
||||
}
|
||||
codeObj = res.data;
|
||||
$(".client_id").html(res.data.clientId);
|
||||
$(".scope").html(res.data.scope);
|
||||
},
|
||||
error: function(e, ee, eee) {
|
||||
console.log('error');
|
||||
alert(eee);
|
||||
}
|
||||
});
|
||||
|
||||
// 确认授权
|
||||
function ok() {
|
||||
$.ajax({
|
||||
url: '/oauth2/confirm',
|
||||
data: {code: code},
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if(res.code == 200) {
|
||||
// 跳转
|
||||
location.href = codeObj.redirectUri;
|
||||
} else {
|
||||
alert(res.msg);
|
||||
}
|
||||
},
|
||||
error: function(e, ee, eee) {
|
||||
alert(eee);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 拒绝授权时,跳入拒绝授权地址
|
||||
function no() {
|
||||
// alert(codeObj.rejectUri)
|
||||
location.href = codeObj.rejectUri;
|
||||
}
|
||||
|
||||
// 从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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,53 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>服务提供方-登录页</title>
|
||||
<style type="text/css">
|
||||
*{margin: 0px; padding: 0px;}
|
||||
.login-box{width: 400px; margin: 50px auto;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>服务提供方-登录页</h2> <br>
|
||||
<div>注:您当前在服务提供方尚未登录,请先登录</div>
|
||||
<div>测试账号: test test</div> <br>
|
||||
账号:<input name="username" /> <br>
|
||||
密码:<input name="password" type="password" /> <br>
|
||||
<button onclick="doLogin()">登录</button>
|
||||
</div>
|
||||
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
// 登录方法
|
||||
function getLoginId() {
|
||||
|
||||
}
|
||||
|
||||
// 登录方法
|
||||
function doLogin() {
|
||||
console.log('-----------');
|
||||
$.ajax({
|
||||
url: '/doLogin',
|
||||
data: {
|
||||
username: $('[name=username]').val(),
|
||||
password: $('[name=password]').val()
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if(res.code == 200) {
|
||||
alert('登录成功');
|
||||
location.reload(true);
|
||||
} else {
|
||||
alert('登录失败');
|
||||
}
|
||||
},
|
||||
error: function(e) {
|
||||
console.log('error');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Sa-OAuth2-认证中心-确认授权页</title>
|
||||
<style type="text/css">
|
||||
body{background-color: #F5F5D5;}
|
||||
*{margin: 0px; padding: 0px;}
|
||||
.login-box{width: 400px; margin: 20vh auto; padding: 70px; border: 1px #000 solid;}
|
||||
.login-box button{padding: 5px 15px; cursor: pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>Sa-OAuth2-认证中心-确认授权页</h2> <br>
|
||||
<div>
|
||||
<div><b>应用ID:</b><span th:utext="${clientId}"></span></div>
|
||||
<div><b>请求授权:</b><span th:utext="${scope}"></span></div>
|
||||
<br><div>------------- 是否同意授权 -------------</div><br>
|
||||
<div>
|
||||
<button onclick="yes()">同意</button>
|
||||
<button onclick="no()">拒绝</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
|
||||
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
|
||||
<script type="text/javascript">
|
||||
|
||||
// 同意授权
|
||||
function yes() {
|
||||
console.log('-----------');
|
||||
$.ajax({
|
||||
url: '/oauth2/doConfirm',
|
||||
data: {
|
||||
client_id: getParam('client_id'),
|
||||
scope: getParam('scope')
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if(res.code == 200) {
|
||||
layer.msg('授权成功!');
|
||||
setTimeout(function() {
|
||||
location.reload(true);
|
||||
}, 800);
|
||||
} else {
|
||||
// 重定向至授权失败URL
|
||||
layer.alert('授权失败!');
|
||||
}
|
||||
},
|
||||
error: function(e) {
|
||||
console.log('error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 拒绝授权
|
||||
function no() {
|
||||
var url = joinParam(getParam('redirect_uri'), "handle=refuse&msg=用户拒绝了授权");
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
// 从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);
|
||||
}
|
||||
|
||||
// 在url上拼接上kv参数并返回
|
||||
function joinParam(url, parameStr) {
|
||||
if(parameStr == null || parameStr.length == 0) {
|
||||
return url;
|
||||
}
|
||||
var index = url.indexOf('?');
|
||||
// ? 不存在
|
||||
if(index == -1) {
|
||||
return url + '?' + parameStr;
|
||||
}
|
||||
// ? 是最后一位
|
||||
if(index == url.length - 1) {
|
||||
return url + parameStr;
|
||||
}
|
||||
// ? 是其中一位
|
||||
if(index > -1 && index < url.length - 1) {
|
||||
// 如果最后一位是 不是&, 且 parameStr 第一位不是 &, 就增送一个 &
|
||||
if(url.lastIndexOf('&') != url.length - 1 && parameStrindexOf('&') != 0) {
|
||||
return url + '&' + parameStr;
|
||||
} else {
|
||||
return url + parameStr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Sa-OAuth2-认证中心-登录页</title>
|
||||
<style type="text/css">
|
||||
body{background-color: #F5F5D5;}
|
||||
*{margin: 0px; padding: 0px;}
|
||||
.login-box{width: 400px; margin: 20vh auto;}
|
||||
.login-box input{line-height: 25px; margin-bottom: 10px;}
|
||||
.login-box button{padding: 5px 15px; cursor: pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>Sa-OAuth2-认证中心-登录页</h2> <br>
|
||||
账号:<input name="name" /> <br>
|
||||
密码:<input name="pwd" type="password" /> <br>
|
||||
<button onclick="doLogin()">登录</button>
|
||||
<span style="color: #666;">(测试账号: sa 123456)</span>
|
||||
</div>
|
||||
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
|
||||
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
|
||||
<script type="text/javascript">
|
||||
|
||||
// 登录方法
|
||||
function doLogin() {
|
||||
console.log('-----------');
|
||||
$.ajax({
|
||||
url: '/oauth2/doLogin',
|
||||
data: {
|
||||
name: $('[name=name]').val(),
|
||||
pwd: $('[name=pwd]').val()
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if(res.code == 200) {
|
||||
layer.msg('登录成功!');
|
||||
setTimeout(function() {
|
||||
location.reload(true);
|
||||
}, 800);
|
||||
} else {
|
||||
layer.alert(res.msg);
|
||||
}
|
||||
},
|
||||
error: function(e) {
|
||||
console.log('error');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<!-- 定义sa-token版本号 -->
|
||||
<properties>
|
||||
<sa-token-version>1.21.0</sa-token-version>
|
||||
<sa-token-version>1.26.0</sa-token-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -16,6 +16,10 @@ sa:
|
||||
title: Sa-Token 登录
|
||||
# 是否显示底部版权信息
|
||||
copr: true
|
||||
# 指定拦截路径
|
||||
# include: /**
|
||||
# 指定排除路径
|
||||
# exclude: /sss,/fff
|
||||
# 将本地磁盘的某个路径作为静态资源开放
|
||||
# dir: file:E:\static
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<!-- 定义sa-token版本号 -->
|
||||
<properties>
|
||||
<sa-token-version>1.21.0</sa-token-version>
|
||||
<sa-token-version>1.26.0</sa-token-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -11,17 +11,18 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.0.0.RELEASE</version>
|
||||
<!-- <version>1.5.9.RELEASE</version> -->
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<!-- 定义sa-token版本号 -->
|
||||
<properties>
|
||||
<sa-token-version>1.21.0</sa-token-version>
|
||||
<sa-token-version>1.26.0</sa-token-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- springboot依赖 -->
|
||||
<!-- SpringBoot依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
@@ -38,32 +39,32 @@
|
||||
<version>${sa-token-version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- sa-token整合redis (使用jdk默认序列化方式) -->
|
||||
<!-- Sa-Token 整合 Redis (使用jdk默认序列化方式) -->
|
||||
<!-- <dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-dao-redis</artifactId>
|
||||
<version>${sa-token-version}</version>
|
||||
</dependency> -->
|
||||
|
||||
<!-- sa-token整合redis (使用jackson序列化方式) -->
|
||||
<!-- Sa-Token整合 Redis (使用jackson序列化方式) -->
|
||||
<!-- <dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-dao-redis-jackson</artifactId>
|
||||
<version>${sa-token-version}</version>
|
||||
</dependency> -->
|
||||
|
||||
<!-- 提供redis连接池 -->
|
||||
<!-- 提供Redis连接池 -->
|
||||
<!-- <dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency> -->
|
||||
|
||||
<!-- sa-token整合SpringAOP实现注解鉴权 -->
|
||||
<dependency>
|
||||
<!-- Sa-Token整合SpringAOP实现注解鉴权 -->
|
||||
<!-- <dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-aop</artifactId>
|
||||
<version>${sa-token-version}</version>
|
||||
</dependency>
|
||||
</dependency> -->
|
||||
|
||||
<!-- @ConfigurationProperties -->
|
||||
<dependency>
|
||||
@@ -79,7 +80,6 @@
|
||||
<version>5.5.4</version>
|
||||
</dependency> -->
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -68,5 +68,6 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.pj.satoken.at.StpUserUtil;
|
||||
import com.pj.util.AjaxJson;
|
||||
import com.pj.util.Ttime;
|
||||
|
||||
@@ -30,7 +29,6 @@ import cn.dev33.satoken.stp.StpUtil;
|
||||
@RequestMapping("/test/")
|
||||
public class TestController {
|
||||
|
||||
|
||||
// 测试登录接口, 浏览器访问: http://localhost:8081/test/login
|
||||
@RequestMapping("login")
|
||||
public AjaxJson login(@RequestParam(defaultValue="10001") String id) {
|
||||
@@ -242,7 +240,6 @@ public class TestController {
|
||||
@RequestMapping("test")
|
||||
public AjaxJson test() {
|
||||
System.out.println("进来了");
|
||||
StpUserUtil.login(10001);
|
||||
return AjaxJson.getSuccess();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ sa-token:
|
||||
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
|
||||
activity-timeout: -1
|
||||
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
|
||||
is-concurrent: false
|
||||
is-concurrent: true
|
||||
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
|
||||
is-share: true
|
||||
# token风格
|
||||
@@ -30,8 +30,8 @@ spring:
|
||||
port: 6379
|
||||
# Redis服务器连接密码(默认为空)
|
||||
password:
|
||||
# 连接超时时间(毫秒)
|
||||
timeout: 10ms
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
pool:
|
||||
# 连接池最大连接数
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
target/
|
||||
|
||||
node_modules/
|
||||
bin/
|
||||
.settings/
|
||||
unpackage/
|
||||
.classpath
|
||||
.project
|
||||
|
||||
.idea/
|
||||
|
||||
.factorypath
|
||||
@@ -0,0 +1,60 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-demo-sso1-client</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<!-- SpringBoot -->
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.0.0.RELEASE</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<!-- 定义sa-token版本号 -->
|
||||
<properties>
|
||||
<sa-token-version>1.26.0</sa-token-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- 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>${sa-token-version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.pj;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* SSO模式一,Client端 Demo
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class SaSsoClientApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SaSsoClientApplication.class, args);
|
||||
System.out.println("\nSa-Token-SSO Client端启动成功");
|
||||
}
|
||||
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.pj.sso;
|
||||
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import cn.dev33.satoken.SaManager;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
|
||||
/**
|
||||
* Sa-Token-SSO Client端 Controller
|
||||
* @author kong
|
||||
*/
|
||||
@RestController
|
||||
public class SsoClientController {
|
||||
|
||||
/*
|
||||
* SSO-Client端:首页
|
||||
*/
|
||||
@RequestMapping("/")
|
||||
public String index() {
|
||||
String authUrl = SaManager.getConfig().getSso().getAuthUrl();
|
||||
String solUrl = SaManager.getConfig().getSso().getSloUrl();
|
||||
String str = "<h2>Sa-Token SSO-Client 应用端</h2>" +
|
||||
"<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
|
||||
"<p><a href=\"javascript:location.href='" + authUrl + "?redirect=' + encodeURIComponent(location.href);\">登录</a> " +
|
||||
"<a href=\"javascript:location.href='" + solUrl + "?back=' + encodeURIComponent(location.href);\">注销</a> </p>";
|
||||
return str;
|
||||
}
|
||||
|
||||
// 全局异常拦截
|
||||
@ExceptionHandler
|
||||
public SaResult handlerException(Exception e) {
|
||||
e.printStackTrace();
|
||||
return SaResult.error(e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# 端口
|
||||
server:
|
||||
port: 9001
|
||||
|
||||
# sa-token配置
|
||||
sa-token:
|
||||
# SSO-相关配置
|
||||
sso:
|
||||
# SSO-Server端 单点登录地址
|
||||
auth-url: http://sso.stp.com:9000/sso/auth
|
||||
# SSO-Server端 单点注销地址
|
||||
slo-url: http://sso.stp.com:9000/sso/logout
|
||||
|
||||
# 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis)
|
||||
alone-redis:
|
||||
# Redis数据库索引
|
||||
database: 1
|
||||
# Redis服务器地址
|
||||
host: 127.0.0.1
|
||||
# Redis服务器连接端口
|
||||
port: 6379
|
||||
# Redis服务器连接密码(默认为空)
|
||||
password:
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
pool:
|
||||
# 连接池最大连接数
|
||||
max-active: 200
|
||||
# 连接池最大阻塞等待时间(使用负值表示没有限制)
|
||||
max-wait: -1ms
|
||||
# 连接池中的最大空闲连接
|
||||
max-idle: 10
|
||||
# 连接池中的最小空闲连接
|
||||
min-idle: 0
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
target/
|
||||
|
||||
node_modules/
|
||||
bin/
|
||||
.settings/
|
||||
unpackage/
|
||||
.classpath
|
||||
.project
|
||||
|
||||
.idea/
|
||||
|
||||
.factorypath
|
||||
@@ -0,0 +1,57 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-demo-sso1-server</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<!-- SpringBoot -->
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.0.0.RELEASE</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<!-- 定义sa-token版本号 -->
|
||||
<properties>
|
||||
<sa-token-version>1.26.0</sa-token-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- 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>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 视图引擎 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.pj;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* SSO模式一,Server端 Demo
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class SaSsoServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SaSsoServerApplication.class, args);
|
||||
System.out.println("\nSa-Token-SSO 认证中心启动成功");
|
||||
}
|
||||
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.pj.sso;
|
||||
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
|
||||
/**
|
||||
* Sa-Token-SSO Server端 Controller
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
@RestController
|
||||
public class SsoServerController {
|
||||
|
||||
/*
|
||||
* SSO-Server端:统一登录地址
|
||||
*/
|
||||
@RequestMapping("/sso/auth")
|
||||
public Object ssoAuth(String redirect) {
|
||||
// 如果尚未登录,则返回登录视图进行登录
|
||||
if(StpUtil.isLogin() == false) {
|
||||
return new ModelAndView("sa-login.html");
|
||||
}
|
||||
// 如果已登录,则原路返回到 Client端
|
||||
return SaHolder.getResponse().redirect(redirect);
|
||||
}
|
||||
|
||||
// 处理登录请求
|
||||
@RequestMapping("/sso/doLogin")
|
||||
public SaResult ssoDoLogin(String name, String pwd) {
|
||||
// 模拟登录
|
||||
if("sa".equals(name) && "123456".equals(pwd)) {
|
||||
StpUtil.login(10001);
|
||||
return SaResult.ok("登录成功!");
|
||||
}
|
||||
return SaResult.error("登录失败!");
|
||||
}
|
||||
|
||||
// 单点注销地址
|
||||
@RequestMapping("/sso/logout")
|
||||
public Object ssoLogout(String back) {
|
||||
StpUtil.logout();
|
||||
if(SaFoxUtil.isEmpty(back)) {
|
||||
return SaResult.ok();
|
||||
}
|
||||
return SaHolder.getResponse().redirect(back);
|
||||
}
|
||||
|
||||
// 全局异常拦截
|
||||
@ExceptionHandler
|
||||
public SaResult handlerException(Exception e) {
|
||||
e.printStackTrace();
|
||||
return SaResult.error(e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# 端口
|
||||
server:
|
||||
port: 9000
|
||||
|
||||
# Sa-Token配置
|
||||
sa-token:
|
||||
# 写入Cookie时显式指定的作用域, 用于单点登录二级域名共享Cookie
|
||||
cookie-domain: stp.com
|
||||
|
||||
spring:
|
||||
# Redis配置
|
||||
redis:
|
||||
# Redis数据库索引(默认为0)
|
||||
database: 1
|
||||
# Redis服务器地址
|
||||
host: 127.0.0.1
|
||||
# Redis服务器连接端口
|
||||
port: 6379
|
||||
# Redis服务器连接密码(默认为空)
|
||||
password:
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
pool:
|
||||
# 连接池最大连接数
|
||||
max-active: 200
|
||||
# 连接池最大阻塞等待时间(使用负值表示没有限制)
|
||||
max-wait: -1ms
|
||||
# 连接池中的最大空闲连接
|
||||
max-idle: 10
|
||||
# 连接池中的最小空闲连接
|
||||
min-idle: 0
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
*{margin: 0; padding: 0;}
|
||||
body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;}
|
||||
::-webkit-input-placeholder{color: #ccc;}
|
||||
|
||||
/* 视图盒子 */
|
||||
.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;}
|
||||
/* 背景 EAEFF3 */
|
||||
.bg-1{height: 50%; background: linear-gradient(to bottom right, #0466c5, #3496F5);}
|
||||
.bg-2{height: 50%; background-color: #EAEFF3;}
|
||||
|
||||
/* 渐变背景 */
|
||||
/*.bg-1{
|
||||
background-size: 500%;
|
||||
background-image: linear-gradient(125deg,#0466c5,#3496F5,#0466c5,#3496F5,#0466c5,#2496F5);
|
||||
animation: bganimation 30s infinite;
|
||||
}
|
||||
@keyframes bganimation{
|
||||
0%{background-position: 0% 50%;}
|
||||
50%{background-position: 100% 50%;}
|
||||
100%{background-position: 0% 50%;}
|
||||
} */
|
||||
/* 背景 */
|
||||
.bg-1{background: #101C34;}
|
||||
.bg-2{background: #101C34;}
|
||||
/* .bg-1{height: 100%; background-image: url(./login-bg.png); background-size: 100% 100%;} */
|
||||
|
||||
|
||||
/* 内容盒子 */
|
||||
.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;}
|
||||
|
||||
/* 登录盒子 */
|
||||
/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */
|
||||
.login-box{width: 400px; margin: auto; max-width: 90%; height: 100%;}
|
||||
.login-box{display: flex; align-items: center; text-align: center;}
|
||||
|
||||
/* 表单 */
|
||||
.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;}
|
||||
.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;}
|
||||
.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;}
|
||||
|
||||
/* 输入框 */
|
||||
.from-item{border: 0px #000 solid; margin-bottom: 15px;}
|
||||
.s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;}
|
||||
.s-input{font-size: 12px;}
|
||||
.s-input:focus{border-color: #409eff}
|
||||
|
||||
/* 登录按钮 */
|
||||
.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;}
|
||||
.s-btn:hover{background-color: #50aEFF;}
|
||||
|
||||
/* 重置按钮 */
|
||||
.reset-box{text-align: left; font-size: 12px;}
|
||||
.reset-box a{text-decoration: none;}
|
||||
.reset-box a:hover{text-decoration: underline;}
|
||||
|
||||
/* loading框样式 */
|
||||
.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}
|
||||
.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}
|
||||
.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }
|
||||
@@ -0,0 +1,65 @@
|
||||
// sa
|
||||
var sa = {};
|
||||
|
||||
// 打开loading
|
||||
sa.loading = function(msg) {
|
||||
layer.closeAll(); // 开始前先把所有弹窗关了
|
||||
return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' });
|
||||
};
|
||||
|
||||
// 隐藏loading
|
||||
sa.hideLoading = function() {
|
||||
layer.closeAll();
|
||||
};
|
||||
|
||||
|
||||
// ----------------------------------- 登录事件 -----------------------------------
|
||||
|
||||
$('.login-btn').click(function(){
|
||||
sa.loading("正在登录...");
|
||||
// 开始登录
|
||||
setTimeout(function() {
|
||||
$.ajax({
|
||||
url: "sso/doLogin",
|
||||
type: "post",
|
||||
data: {
|
||||
name: $('[name=name]').val(),
|
||||
pwd: $('[name=pwd]').val()
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
console.log('返回数据:', res);
|
||||
sa.hideLoading();
|
||||
if(res.code == 200) {
|
||||
layer.msg('登录成功', {anim: 0, icon: 6 });
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 800)
|
||||
} else {
|
||||
layer.msg(res.msg, {anim: 6, icon: 2 });
|
||||
}
|
||||
},
|
||||
error: function(xhr, type, errorThrown){
|
||||
sa.hideLoading();
|
||||
if(xhr.status == 0){
|
||||
return layer.alert('无法连接到服务器,请检查网络');
|
||||
}
|
||||
return layer.alert("异常:" + JSON.stringify(xhr));
|
||||
}
|
||||
});
|
||||
}, 400);
|
||||
});
|
||||
|
||||
// 绑定回车事件
|
||||
$('[name=name],[name=pwd]').bind('keypress', function(event){
|
||||
if(event.keyCode == "13") {
|
||||
$('.login-btn').click();
|
||||
}
|
||||
});
|
||||
|
||||
// 输入框获取焦点
|
||||
$("[name=name]").focus();
|
||||
|
||||
// 打印信息
|
||||
var str = "This page is provided by Sa-Token, Please refer to: " + "http://sa-token.dev33.cn/";
|
||||
console.log(str);
|
||||
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<title>Sa-SSO-Server 认证中心-登录</title>
|
||||
<meta charset="utf-8">
|
||||
<base th:href="@{/}" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<link rel="stylesheet" href="./sa-res/login.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="view-box">
|
||||
<div class="bg-1"></div>
|
||||
<div class="bg-2"></div>
|
||||
<div class="content-box">
|
||||
<div class="login-box">
|
||||
<div class="from-box">
|
||||
<h2 class="from-title">Sa-SSO-Server 认证中心</h2>
|
||||
<div class="from-item">
|
||||
<input class="s-input" name="name" placeholder="请输入账号" />
|
||||
</div>
|
||||
<div class="from-item">
|
||||
<input class="s-input" name="pwd" type="password" placeholder="请输入密码" />
|
||||
</div>
|
||||
<div class="from-item">
|
||||
<button class="s-input s-btn login-btn">登录</button>
|
||||
</div>
|
||||
<div class="from-item reset-box">
|
||||
<a href="javascript: location.reload();" >刷新</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部 版权 -->
|
||||
<div style="position: absolute; bottom: 40px; width: 100%; text-align: center; color: #666;">
|
||||
This page is provided by Sa-Token-SSO
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- scripts -->
|
||||
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
|
||||
<script src="./sa-res/login.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义sa-token版本号 -->
|
||||
<properties>
|
||||
<sa-token-version>1.21.0</sa-token-version>
|
||||
<sa-token-version>1.26.0</sa-token-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -8,9 +8,29 @@ import cn.dev33.satoken.SaManager;
|
||||
@SpringBootApplication
|
||||
public class SaSsoApplication {
|
||||
|
||||
/*
|
||||
此 demo 意在使用最少的代码演示一下SSO模式一的认证原理
|
||||
1、改hosts(C:\windows\system32\drivers\etc\hosts)
|
||||
127.0.0.1 sso.stp.com
|
||||
127.0.0.1 s1.stp.com
|
||||
127.0.0.1 s2.stp.com
|
||||
127.0.0.1 s3.stp.com
|
||||
2、运行项目
|
||||
启动 SaSsoApplication
|
||||
3、浏览器访问
|
||||
http://s1.stp.com:8081/sso/isLogin
|
||||
http://s2.stp.com:8081/sso/isLogin
|
||||
http://s3.stp.com:8081/sso/isLogin
|
||||
均显示未登录
|
||||
4、然后访问任意节点的登录接口:
|
||||
http://s1.stp.com:8081/sso/doLogin
|
||||
5、重复步骤3,刷新三个地址
|
||||
均显示已登录
|
||||
*/
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SaSsoApplication.class, args);
|
||||
System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -4,9 +4,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.pj.util.AjaxJson;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
|
||||
/**
|
||||
* 测试: 同域单点登录
|
||||
@@ -15,21 +14,21 @@ import cn.dev33.satoken.stp.StpUtil;
|
||||
@RestController
|
||||
@RequestMapping("/sso/")
|
||||
public class SsoController {
|
||||
|
||||
|
||||
// 测试:进行登录
|
||||
@RequestMapping("doLogin")
|
||||
public AjaxJson doLogin(@RequestParam(defaultValue = "10001") String id) {
|
||||
public SaResult doLogin(@RequestParam(defaultValue = "10001") String id) {
|
||||
System.out.println("---------------- 进行登录 ");
|
||||
StpUtil.login(id);
|
||||
return AjaxJson.getSuccess("登录成功: " + id);
|
||||
return SaResult.ok("登录成功: " + id);
|
||||
}
|
||||
|
||||
// 测试:是否登录
|
||||
@RequestMapping("isLogin")
|
||||
public AjaxJson isLogin() {
|
||||
public SaResult isLogin() {
|
||||
System.out.println("---------------- 是否登录 ");
|
||||
boolean isLogin = StpUtil.isLogin();
|
||||
return AjaxJson.getSuccess("是否登录: " + isLogin);
|
||||
return SaResult.ok("是否登录: " + isLogin);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
package com.pj.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* ajax请求返回Json格式数据的封装
|
||||
*/
|
||||
public class AjaxJson implements Serializable{
|
||||
|
||||
private static final long serialVersionUID = 1L; // 序列化版本号
|
||||
|
||||
public static final int CODE_SUCCESS = 200; // 成功状态码
|
||||
public static final int CODE_ERROR = 500; // 错误状态码
|
||||
public static final int CODE_WARNING = 501; // 警告状态码
|
||||
public static final int CODE_NOT_JUR = 403; // 无权限状态码
|
||||
public static final int CODE_NOT_LOGIN = 401; // 未登录状态码
|
||||
public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码
|
||||
|
||||
public int code; // 状态码
|
||||
public String msg; // 描述信息
|
||||
public Object data; // 携带对象
|
||||
public Long dataCount; // 数据总数,用于分页
|
||||
|
||||
/**
|
||||
* 返回code
|
||||
* @return
|
||||
*/
|
||||
public int getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 给msg赋值,连缀风格
|
||||
*/
|
||||
public AjaxJson setMsg(String msg) {
|
||||
this.msg = msg;
|
||||
return this;
|
||||
}
|
||||
public String getMsg() {
|
||||
return this.msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 给data赋值,连缀风格
|
||||
*/
|
||||
public AjaxJson setData(Object data) {
|
||||
this.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将data还原为指定类型并返回
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getData(Class<T> cs) {
|
||||
return (T) data;
|
||||
}
|
||||
|
||||
// ============================ 构建 ==================================
|
||||
|
||||
public AjaxJson(int code, String msg, Object data, Long dataCount) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
this.dataCount = dataCount;
|
||||
}
|
||||
|
||||
// 返回成功
|
||||
public static AjaxJson getSuccess() {
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", null, null);
|
||||
}
|
||||
public static AjaxJson getSuccess(String msg) {
|
||||
return new AjaxJson(CODE_SUCCESS, msg, null, null);
|
||||
}
|
||||
public static AjaxJson getSuccess(String msg, Object data) {
|
||||
return new AjaxJson(CODE_SUCCESS, msg, data, null);
|
||||
}
|
||||
public static AjaxJson getSuccessData(Object data) {
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
|
||||
}
|
||||
public static AjaxJson getSuccessArray(Object... data) {
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
|
||||
}
|
||||
|
||||
// 返回失败
|
||||
public static AjaxJson getError() {
|
||||
return new AjaxJson(CODE_ERROR, "error", null, null);
|
||||
}
|
||||
public static AjaxJson getError(String msg) {
|
||||
return new AjaxJson(CODE_ERROR, msg, null, null);
|
||||
}
|
||||
|
||||
// 返回警告
|
||||
public static AjaxJson getWarning() {
|
||||
return new AjaxJson(CODE_ERROR, "warning", null, null);
|
||||
}
|
||||
public static AjaxJson getWarning(String msg) {
|
||||
return new AjaxJson(CODE_WARNING, msg, null, null);
|
||||
}
|
||||
|
||||
// 返回未登录
|
||||
public static AjaxJson getNotLogin() {
|
||||
return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
|
||||
}
|
||||
|
||||
// 返回没有权限的
|
||||
public static AjaxJson getNotJur(String msg) {
|
||||
return new AjaxJson(CODE_NOT_JUR, msg, null, null);
|
||||
}
|
||||
|
||||
// 返回一个自定义状态码的
|
||||
public static AjaxJson get(int code, String msg){
|
||||
return new AjaxJson(code, msg, null, null);
|
||||
}
|
||||
|
||||
// 返回分页和数据的
|
||||
public static AjaxJson getPageData(Long dataCount, Object data){
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
|
||||
}
|
||||
|
||||
// 返回,根据受影响行数的(大于0=ok,小于0=error)
|
||||
public static AjaxJson getByLine(int line){
|
||||
if(line > 0){
|
||||
return getSuccess("ok", line);
|
||||
}
|
||||
return getError("error").setData(line);
|
||||
}
|
||||
|
||||
// 返回,根据布尔值来确定最终结果的 (true=ok,false=error)
|
||||
public static AjaxJson getByBoolean(boolean b){
|
||||
return b ? getSuccess("ok") : getError("error");
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
public String toString() {
|
||||
String data_string = null;
|
||||
if(data == null){
|
||||
|
||||
} else if(data instanceof List){
|
||||
data_string = "List(length=" + ((List)data).size() + ")";
|
||||
} else {
|
||||
data_string = data.toString();
|
||||
}
|
||||
return "{"
|
||||
+ "\"code\": " + this.getCode()
|
||||
+ ", \"msg\": \"" + this.getMsg() + "\""
|
||||
+ ", \"data\": " + data_string
|
||||
+ ", \"dataCount\": " + dataCount
|
||||
+ "}";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Sa-Token-SSO-Client端-测试页(前后端分离版)</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Sa-Token SSO-Client 应用端(前后端分离版)</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>
|
||||
</p>
|
||||
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
// 后端接口地址
|
||||
var baseUrl = "http://sa-sso-client1.com:9001";
|
||||
|
||||
// 查询当前会话是否登录
|
||||
$.ajax({
|
||||
url: baseUrl + '/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));
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,91 @@
|
||||
<!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="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
|
||||
<script type="text/javascript">
|
||||
|
||||
// 后端接口地址
|
||||
var baseUrl = "http://sa-sso-client1.com:9001";
|
||||
|
||||
var back = getParam('back', '/');
|
||||
var ticket = getParam('ticket');
|
||||
$(function() {
|
||||
if(ticket) {
|
||||
doLoginByTicket(ticket);
|
||||
} else {
|
||||
goSsoAuthUrl();
|
||||
}
|
||||
})
|
||||
|
||||
// 重定向至认证中心
|
||||
function goSsoAuthUrl() {
|
||||
sa.ajax('/getSsoAuthUrl', {clientLoginUrl: location.href}, function(res) {
|
||||
console.log(res);
|
||||
location.href = res.data;
|
||||
})
|
||||
}
|
||||
|
||||
// 根据ticket值登录
|
||||
function doLoginByTicket(ticket) {
|
||||
sa.ajax('/doLoginByTicket', {ticket: ticket}, function(res) {
|
||||
console.log(res);
|
||||
if(res.code == 200) {
|
||||
localStorage.setItem('satoken', res.data);
|
||||
location.href = decodeURIComponent(back);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 从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);
|
||||
}
|
||||
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
var sa = {};
|
||||
|
||||
// 封装一下Ajax
|
||||
sa.ajax = function(url, data, successFn) {
|
||||
// sa.loading("正在努力加载...");
|
||||
$.ajax({
|
||||
url: baseUrl + url,
|
||||
type: "post",
|
||||
data: data,
|
||||
dataType: 'json',
|
||||
headers: {
|
||||
satoken: localStorage.getItem('satoken')
|
||||
},
|
||||
success: function(res){
|
||||
console.log('返回数据:', res);
|
||||
successFn(res);
|
||||
},
|
||||
error: function(xhr, type, errorThrown){
|
||||
if(xhr.status == 0){
|
||||
return alert('无法连接到服务器,请检查网络');
|
||||
}
|
||||
return alert("异常:" + JSON.stringify(xhr));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义sa-token版本号 -->
|
||||
<properties>
|
||||
<sa-token-version>1.21.0</sa-token-version>
|
||||
<sa-token-version>1.26.0</sa-token-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -51,7 +51,7 @@
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-alone-redis</artifactId>
|
||||
<version>1.21.0</version>
|
||||
<version>${sa-token-version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.pj.h5;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 跨域过滤器
|
||||
* @author kong
|
||||
*/
|
||||
@Component
|
||||
@Order(-200)
|
||||
public class CorsFilter implements Filter {
|
||||
|
||||
static final String OPTIONS = "OPTIONS";
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest request = (HttpServletRequest) req;
|
||||
HttpServletResponse response = (HttpServletResponse) res;
|
||||
|
||||
// 允许指定域访问跨域资源
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
// 允许所有请求方式
|
||||
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
|
||||
// 有效时间
|
||||
response.setHeader("Access-Control-Max-Age", "3600");
|
||||
// 允许的header参数
|
||||
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,satoken");
|
||||
|
||||
// 如果是预检请求,直接返回
|
||||
if (OPTIONS.equals(request.getMethod())) {
|
||||
System.out.println("=======================浏览器发来了OPTIONS预检请求==========");
|
||||
response.getWriter().print("");
|
||||
return;
|
||||
}
|
||||
|
||||
// System.out.println("*********************************过滤器被使用**************************");
|
||||
chain.doFilter(req, res);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.pj.h5;
|
||||
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import cn.dev33.satoken.sso.SaSsoUtil;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
|
||||
/**
|
||||
* 前后台分离架构下集成SSO所需的代码 (SSO-Client端)
|
||||
* <p>(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)</p>
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
@RestController
|
||||
public class H5Controller {
|
||||
|
||||
// 当前是否登录
|
||||
@RequestMapping("/isLogin")
|
||||
public Object isLogin() {
|
||||
return SaResult.data(StpUtil.isLogin());
|
||||
}
|
||||
|
||||
// 返回SSO认证中心登录地址
|
||||
@RequestMapping("/getSsoAuthUrl")
|
||||
public SaResult getSsoAuthUrl(String clientLoginUrl) {
|
||||
String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(clientLoginUrl, "");
|
||||
return SaResult.data(serverAuthUrl);
|
||||
}
|
||||
|
||||
// 根据ticket进行登录
|
||||
@RequestMapping("/doLoginByTicket")
|
||||
public SaResult doLoginByTicket(String ticket) {
|
||||
Object loginId = checkTicket(ticket);
|
||||
if(loginId != null) {
|
||||
StpUtil.login(loginId);
|
||||
return SaResult.data(StpUtil.getTokenValue());
|
||||
}
|
||||
return SaResult.error("无效ticket:" + ticket);
|
||||
}
|
||||
|
||||
// 校验 Ticket码,获取账号Id
|
||||
private Object checkTicket(String ticket) {
|
||||
return SaSsoUtil.checkTicket(ticket);
|
||||
}
|
||||
|
||||
// 全局异常拦截
|
||||
@ExceptionHandler
|
||||
public SaResult handlerException(Exception e) {
|
||||
e.printStackTrace();
|
||||
return SaResult.error(e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
+23
-36
@@ -1,12 +1,12 @@
|
||||
package com.pj.sso;
|
||||
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.sso.SaSsoUtil;
|
||||
import cn.dev33.satoken.sso.SaSsoHandle;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
|
||||
/**
|
||||
* Sa-Token-SSO Client端 Controller
|
||||
@@ -15,45 +15,32 @@ import cn.dev33.satoken.stp.StpUtil;
|
||||
@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=' + encodeURIComponent(location.href);\">登录</a></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端:登录地址
|
||||
@RequestMapping("ssoLogin")
|
||||
public Object ssoLogin(String back, String ticket) {
|
||||
// 如果当前Client端已经登录,则无需访问SSO认证中心,可以直接返回
|
||||
if(StpUtil.isLogin()) {
|
||||
return new ModelAndView("redirect:" + back);
|
||||
}
|
||||
/*
|
||||
* 接下来两种情况:
|
||||
* ticket无值,说明此请求是Client端访问,需要重定向至SSO认证中心
|
||||
* ticket有值,说明此请求从SSO认证中心重定向而来,需要根据ticket进行登录
|
||||
*/
|
||||
if(ticket == null) {
|
||||
String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(SaHolder.getRequest().getUrl(), back);
|
||||
return new ModelAndView("redirect:" + serverAuthUrl);
|
||||
} else {
|
||||
Object loginId = checkTicket(ticket);
|
||||
if(loginId != null ) {
|
||||
// loginId有值,说明ticket有效
|
||||
StpUtil.login(loginId);
|
||||
return new ModelAndView("redirect:" + back);
|
||||
}
|
||||
// 此处向客户端提示ticket无效即可,不要重定向到SSO认证中心,否则容易引起无限重定向
|
||||
return "ticket无效: " + ticket;
|
||||
}
|
||||
}
|
||||
|
||||
// SSO-Client端:校验ticket,获取账号id
|
||||
private Object checkTicket(String ticket) {
|
||||
return SaSsoUtil.checkTicket(ticket);
|
||||
|
||||
/*
|
||||
* 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 SaSsoHandle.clientRequest();
|
||||
}
|
||||
|
||||
// 全局异常拦截
|
||||
@ExceptionHandler
|
||||
public SaResult handlerException(Exception e) {
|
||||
e.printStackTrace();
|
||||
return SaResult.error(e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
package com.pj.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* ajax请求返回Json格式数据的封装
|
||||
*/
|
||||
public class AjaxJson implements Serializable{
|
||||
|
||||
private static final long serialVersionUID = 1L; // 序列化版本号
|
||||
|
||||
public static final int CODE_SUCCESS = 200; // 成功状态码
|
||||
public static final int CODE_ERROR = 500; // 错误状态码
|
||||
public static final int CODE_WARNING = 501; // 警告状态码
|
||||
public static final int CODE_NOT_JUR = 403; // 无权限状态码
|
||||
public static final int CODE_NOT_LOGIN = 401; // 未登录状态码
|
||||
public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码
|
||||
|
||||
public int code; // 状态码
|
||||
public String msg; // 描述信息
|
||||
public Object data; // 携带对象
|
||||
public Long dataCount; // 数据总数,用于分页
|
||||
|
||||
/**
|
||||
* 返回code
|
||||
* @return
|
||||
*/
|
||||
public int getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 给msg赋值,连缀风格
|
||||
*/
|
||||
public AjaxJson setMsg(String msg) {
|
||||
this.msg = msg;
|
||||
return this;
|
||||
}
|
||||
public String getMsg() {
|
||||
return this.msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 给data赋值,连缀风格
|
||||
*/
|
||||
public AjaxJson setData(Object data) {
|
||||
this.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将data还原为指定类型并返回
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getData(Class<T> cs) {
|
||||
return (T) data;
|
||||
}
|
||||
|
||||
// ============================ 构建 ==================================
|
||||
|
||||
public AjaxJson(int code, String msg, Object data, Long dataCount) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
this.dataCount = dataCount;
|
||||
}
|
||||
|
||||
// 返回成功
|
||||
public static AjaxJson getSuccess() {
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", null, null);
|
||||
}
|
||||
public static AjaxJson getSuccess(String msg) {
|
||||
return new AjaxJson(CODE_SUCCESS, msg, null, null);
|
||||
}
|
||||
public static AjaxJson getSuccess(String msg, Object data) {
|
||||
return new AjaxJson(CODE_SUCCESS, msg, data, null);
|
||||
}
|
||||
public static AjaxJson getSuccessData(Object data) {
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
|
||||
}
|
||||
public static AjaxJson getSuccessArray(Object... data) {
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
|
||||
}
|
||||
|
||||
// 返回失败
|
||||
public static AjaxJson getError() {
|
||||
return new AjaxJson(CODE_ERROR, "error", null, null);
|
||||
}
|
||||
public static AjaxJson getError(String msg) {
|
||||
return new AjaxJson(CODE_ERROR, msg, null, null);
|
||||
}
|
||||
|
||||
// 返回警告
|
||||
public static AjaxJson getWarning() {
|
||||
return new AjaxJson(CODE_ERROR, "warning", null, null);
|
||||
}
|
||||
public static AjaxJson getWarning(String msg) {
|
||||
return new AjaxJson(CODE_WARNING, msg, null, null);
|
||||
}
|
||||
|
||||
// 返回未登录
|
||||
public static AjaxJson getNotLogin() {
|
||||
return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
|
||||
}
|
||||
|
||||
// 返回没有权限的
|
||||
public static AjaxJson getNotJur(String msg) {
|
||||
return new AjaxJson(CODE_NOT_JUR, msg, null, null);
|
||||
}
|
||||
|
||||
// 返回一个自定义状态码的
|
||||
public static AjaxJson get(int code, String msg){
|
||||
return new AjaxJson(code, msg, null, null);
|
||||
}
|
||||
|
||||
// 返回分页和数据的
|
||||
public static AjaxJson getPageData(Long dataCount, Object data){
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
|
||||
}
|
||||
|
||||
// 返回,根据受影响行数的(大于0=ok,小于0=error)
|
||||
public static AjaxJson getByLine(int line){
|
||||
if(line > 0){
|
||||
return getSuccess("ok", line);
|
||||
}
|
||||
return getError("error").setData(line);
|
||||
}
|
||||
|
||||
// 返回,根据布尔值来确定最终结果的 (true=ok,false=error)
|
||||
public static AjaxJson getByBoolean(boolean b){
|
||||
return b ? getSuccess("ok") : getError("error");
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
public String toString() {
|
||||
String data_string = null;
|
||||
if(data == null){
|
||||
|
||||
} else if(data instanceof List){
|
||||
data_string = "List(length=" + ((List)data).size() + ")";
|
||||
} else {
|
||||
data_string = data.toString();
|
||||
}
|
||||
return "{"
|
||||
+ "\"code\": " + this.getCode()
|
||||
+ ", \"msg\": \"" + this.getMsg() + "\""
|
||||
+ ", \"data\": " + data_string
|
||||
+ ", \"dataCount\": " + dataCount
|
||||
+ "}";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -4,16 +4,13 @@ server:
|
||||
|
||||
# sa-token配置
|
||||
sa-token:
|
||||
# Token名称
|
||||
token-name: satoken
|
||||
# Token有效期
|
||||
timeout: 2592000
|
||||
# Token风格
|
||||
token-style: uuid
|
||||
# SSO-相关配置
|
||||
sso:
|
||||
# SSO-Server端 单点登录地址
|
||||
auth-url: http://sa-sso-server.com:9000/ssoAuth
|
||||
# SSO-Server端 统一认证地址
|
||||
auth-url: http://sa-sso-server.com:9000/sso/auth
|
||||
# auth-url: http://127.0.0.1:8848/sa-token-demo-sso2-server-h5/sso-auth.html
|
||||
# 是否打开单点注销接口
|
||||
is-slo: true
|
||||
|
||||
# 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis)
|
||||
alone-redis:
|
||||
@@ -25,8 +22,8 @@ sa-token:
|
||||
port: 6379
|
||||
# Redis服务器连接密码(默认为空)
|
||||
password:
|
||||
# 连接超时时间(毫秒)
|
||||
timeout: 10ms
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
pool:
|
||||
# 连接池最大连接数
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
*{margin: 0; padding: 0;}
|
||||
body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;}
|
||||
::-webkit-input-placeholder{color: #ccc;}
|
||||
|
||||
/* 视图盒子 */
|
||||
.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;}
|
||||
/* 背景 EAEFF3 */
|
||||
.bg-1{height: 100%; background: #c0cCf4;}
|
||||
|
||||
/* 内容盒子 */
|
||||
.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;}
|
||||
|
||||
/* 登录盒子 */
|
||||
/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */
|
||||
.login-box{width: 400px; margin: auto; max-width: 90%; height: 100%;}
|
||||
.login-box{display: flex; align-items: center; text-align: center;}
|
||||
|
||||
/* 表单 */
|
||||
.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;}
|
||||
.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;}
|
||||
.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;}
|
||||
|
||||
/* 输入框 */
|
||||
.from-item{border: 0px #000 solid; margin-bottom: 15px;}
|
||||
.s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;}
|
||||
.s-input{font-size: 12px;}
|
||||
.s-input:focus{border-color: #409eff}
|
||||
|
||||
/* 登录按钮 */
|
||||
.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;}
|
||||
.s-btn:hover{background-color: #50aEFF;}
|
||||
|
||||
/* 重置按钮 */
|
||||
.reset-box{text-align: left; font-size: 12px;}
|
||||
.reset-box a{text-decoration: none;}
|
||||
.reset-box a:hover{text-decoration: underline;}
|
||||
|
||||
/* loading框样式 */
|
||||
.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}
|
||||
.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}
|
||||
.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }
|
||||
@@ -0,0 +1,102 @@
|
||||
// 服务端地址
|
||||
var baseUrl = "http://sa-sso-server.com:9000";
|
||||
|
||||
// sa
|
||||
var sa = {};
|
||||
|
||||
// 打开loading
|
||||
sa.loading = function(msg) {
|
||||
layer.closeAll(); // 开始前先把所有弹窗关了
|
||||
return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load'});
|
||||
};
|
||||
|
||||
// 隐藏loading
|
||||
sa.hideLoading = function() {
|
||||
layer.closeAll();
|
||||
};
|
||||
|
||||
// 封装一下Ajax
|
||||
sa.ajax = function(url, data, successFn) {
|
||||
$.ajax({
|
||||
url: baseUrl + url,
|
||||
type: "post",
|
||||
data: data,
|
||||
dataType: 'json',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'satoken': localStorage.getItem('satoken')
|
||||
},
|
||||
success: function(res){
|
||||
console.log('返回数据:', res);
|
||||
successFn(res);
|
||||
},
|
||||
error: function(xhr, type, errorThrown){
|
||||
if(xhr.status == 0){
|
||||
return alert('无法连接到服务器,请检查网络');
|
||||
}
|
||||
return alert("异常:" + JSON.stringify(xhr));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------- 相关事件 -----------------------------------
|
||||
|
||||
// 检查当前是否已经登录,如果已登录则直接开始跳转,如果未登录则等待用户输入账号密码
|
||||
sa.ajax("/getRedirectUrl", {redirect: getParam('redirect', '')}, function(res) {
|
||||
if(res.code == 200) {
|
||||
// redirect地址有效,开始跳转
|
||||
location.href = decodeURIComponent(res.data);
|
||||
} else if(res.code == 401) {
|
||||
console.log('未登录');
|
||||
} else {
|
||||
layer.alert(res.msg);
|
||||
}
|
||||
})
|
||||
|
||||
// 登录
|
||||
$('.login-btn').click(function(){
|
||||
sa.loading("正在登录...");
|
||||
// 开始登录
|
||||
var data = {
|
||||
name: $('[name=name]').val(),
|
||||
pwd: $('[name=pwd]').val()
|
||||
};
|
||||
sa.ajax("/sso/doLogin", data, function(res) {
|
||||
sa.hideLoading();
|
||||
if(res.code == 200) {
|
||||
localStorage.setItem('satoken', res.data);
|
||||
layer.msg('登录成功', {anim: 0, icon: 6 });
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 800);
|
||||
} else {
|
||||
layer.msg(res.msg, {anim: 6, icon: 2 });
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
// 绑定回车事件
|
||||
$('[name=name],[name=pwd]').bind('keypress', function(event){
|
||||
if(event.keyCode == "13") {
|
||||
$('.login-btn').click();
|
||||
}
|
||||
});
|
||||
|
||||
// 输入框获取焦点
|
||||
$("[name=name]").focus();
|
||||
|
||||
// 从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] + (pair[2] ? '=' + pair[2] : '');}
|
||||
}
|
||||
return(defaultValue == undefined ? null : defaultValue);
|
||||
}
|
||||
|
||||
// 打印信息
|
||||
var str = "This page is provided by Sa-Token, Please refer to: " + "http://sa-token.dev33.cn/";
|
||||
console.log(str);
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<title>Sa-SSO-Server 认证中心-登录</title>
|
||||
<meta charset="utf-8">
|
||||
<base th:href="@{/}" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<link rel="stylesheet" href="./login.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="view-box">
|
||||
<div class="bg-1"></div>
|
||||
<div class="content-box">
|
||||
<div class="login-box">
|
||||
<div class="from-box">
|
||||
<h2 class="from-title">Sa-SSO-Server 认证中心(前后端分离版)</h2>
|
||||
<div class="from-item">
|
||||
<input class="s-input" name="name" placeholder="请输入账号" />
|
||||
</div>
|
||||
<div class="from-item">
|
||||
<input class="s-input" name="pwd" type="password" placeholder="请输入密码" />
|
||||
</div>
|
||||
<div class="from-item">
|
||||
<button class="s-input s-btn login-btn">登录</button>
|
||||
</div>
|
||||
<div class="from-item reset-box">
|
||||
<a href="javascript: location.reload();" >刷新</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部 版权 -->
|
||||
<div style="position: absolute; bottom: 40px; width: 100%; text-align: center; color: #666;">
|
||||
This page is provided by Sa-Token-SSO
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- scripts -->
|
||||
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
|
||||
<script src="./login.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义sa-token版本号 -->
|
||||
<properties>
|
||||
<sa-token-version>1.21.0</sa-token-version>
|
||||
<sa-token-version>1.26.0</sa-token-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.pj.h5;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 跨域过滤器
|
||||
* @author kong
|
||||
*/
|
||||
@Component
|
||||
@Order(-200)
|
||||
public class CorsFilter implements Filter {
|
||||
|
||||
static final String OPTIONS = "OPTIONS";
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest request = (HttpServletRequest) req;
|
||||
HttpServletResponse response = (HttpServletResponse) res;
|
||||
|
||||
// 允许指定域访问跨域资源
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
// 允许所有请求方式
|
||||
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
|
||||
// 有效时间
|
||||
response.setHeader("Access-Control-Max-Age", "3600");
|
||||
// 允许的header参数
|
||||
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,satoken");
|
||||
|
||||
// 如果是预检请求,直接返回
|
||||
if (OPTIONS.equals(request.getMethod())) {
|
||||
System.out.println("=======================浏览器发来了OPTIONS预检请求==========");
|
||||
response.getWriter().print("");
|
||||
return;
|
||||
}
|
||||
|
||||
// System.out.println("*********************************过滤器被使用**************************");
|
||||
chain.doFilter(req, res);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.pj.h5;
|
||||
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import cn.dev33.satoken.sso.SaSsoUtil;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
|
||||
/**
|
||||
* 前后台分离架构下集成SSO所需的代码 (SSO-Server端)
|
||||
* <p>(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)</p>
|
||||
* @author kong
|
||||
*
|
||||
*/
|
||||
@RestController
|
||||
public class H5Controller {
|
||||
|
||||
/**
|
||||
* 获取 redirectUrl
|
||||
*/
|
||||
@RequestMapping("/getRedirectUrl")
|
||||
private Object getRedirectUrl(String redirect) {
|
||||
// 未登录情况下,返回 code=401
|
||||
if(StpUtil.isLogin() == false) {
|
||||
return SaResult.code(401);
|
||||
}
|
||||
// 已登录情况下,构建 redirectUrl
|
||||
String redirectUrl = SaSsoUtil.buildRedirectUrl(StpUtil.getLoginId(), redirect);
|
||||
return SaResult.data(redirectUrl);
|
||||
}
|
||||
|
||||
// 全局异常拦截
|
||||
@ExceptionHandler
|
||||
public SaResult handlerException(Exception e) {
|
||||
e.printStackTrace();
|
||||
return SaResult.error(e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.pj.sso;
|
||||
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import com.pj.util.AjaxJson;
|
||||
|
||||
/**
|
||||
* 全局异常处理
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class GlobalException {
|
||||
|
||||
// 全局异常拦截(拦截项目中的所有异常)
|
||||
@ExceptionHandler
|
||||
public AjaxJson handlerException(Exception e) {
|
||||
e.printStackTrace();
|
||||
return AjaxJson.getError(e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
+40
-28
@@ -1,13 +1,15 @@
|
||||
package com.pj.sso;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import com.pj.util.AjaxJson;
|
||||
|
||||
import cn.dev33.satoken.sso.SaSsoUtil;
|
||||
import cn.dev33.satoken.config.SaTokenConfig;
|
||||
import cn.dev33.satoken.sso.SaSsoHandle;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
|
||||
/**
|
||||
* Sa-Token-SSO Server端 Controller
|
||||
@@ -17,33 +19,43 @@ import cn.dev33.satoken.stp.StpUtil;
|
||||
@RestController
|
||||
public class SsoServerController {
|
||||
|
||||
// SSO-Server端:授权地址,跳转到登录页面
|
||||
@RequestMapping("ssoAuth")
|
||||
public Object ssoAuth(String redirect) {
|
||||
/*
|
||||
* 此处两种情况分开处理:
|
||||
* 1、如果在SSO认证中心尚未登录,则先去登登录
|
||||
* 2、如果在SSO认证中心尚已登录,则开始对redirect地址下放ticket引导授权
|
||||
*/
|
||||
// 情况1:尚未登录
|
||||
if(StpUtil.isLogin() == false) {
|
||||
// return "当前会话在SSO-Server端尚未登录,请先访问<a href='/doLogin?name=sa&pwd=123456' target='_blank'> doLogin登录 </a>进行登录之后,刷新页面开始授权";
|
||||
return new ModelAndView("sa-login.html");
|
||||
}
|
||||
// 情况2:已经登录,开始构建授权重定向地址,下放ticket
|
||||
String redirectUrl = SaSsoUtil.buildRedirectUrl(StpUtil.getLoginId(), redirect);
|
||||
return new ModelAndView("redirect:" + redirectUrl);
|
||||
/*
|
||||
* SSO-Server端:处理所有SSO相关请求
|
||||
* http://{host}:{port}/sso/auth -- 单点登录授权地址,接受参数:redirect=授权重定向地址
|
||||
* http://{host}:{port}/sso/doLogin -- 账号密码登录接口,接受参数:name、pwd
|
||||
* http://{host}:{port}/sso/checkTicket -- Ticket校验接口(isHttp=true时打开),接受参数:ticket=ticket码、ssoLogoutCall=单点注销回调地址 [可选]
|
||||
* http://{host}:{port}/sso/logout -- 单点注销地址(isSlo=true时打开),接受参数:loginId=账号id、secretkey=接口调用秘钥
|
||||
*/
|
||||
@RequestMapping("/sso/*")
|
||||
public Object ssoRequest() {
|
||||
return SaSsoHandle.serverRequest();
|
||||
}
|
||||
|
||||
// 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("登录失败!");
|
||||
// 配置SSO相关参数
|
||||
@Autowired
|
||||
private void configSso(SaTokenConfig cfg) {
|
||||
cfg.sso
|
||||
// 配置:未登录时返回的View
|
||||
.setNotLoginView(() -> {
|
||||
return new ModelAndView("sa-login.html");
|
||||
})
|
||||
// 配置:登录处理函数
|
||||
.setDoLoginHandle((name, pwd) -> {
|
||||
// 此处仅做模拟登录,真实环境应该查询数据进行登录
|
||||
if("sa".equals(name) && "123456".equals(pwd)) {
|
||||
StpUtil.login(10001);
|
||||
return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
|
||||
}
|
||||
return SaResult.error("登录失败!");
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
// 全局异常拦截
|
||||
@ExceptionHandler
|
||||
public SaResult handlerException(Exception e) {
|
||||
e.printStackTrace();
|
||||
return SaResult.error(e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
package com.pj.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* ajax请求返回Json格式数据的封装
|
||||
*/
|
||||
public class AjaxJson implements Serializable{
|
||||
|
||||
private static final long serialVersionUID = 1L; // 序列化版本号
|
||||
|
||||
public static final int CODE_SUCCESS = 200; // 成功状态码
|
||||
public static final int CODE_ERROR = 500; // 错误状态码
|
||||
public static final int CODE_WARNING = 501; // 警告状态码
|
||||
public static final int CODE_NOT_JUR = 403; // 无权限状态码
|
||||
public static final int CODE_NOT_LOGIN = 401; // 未登录状态码
|
||||
public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码
|
||||
|
||||
public int code; // 状态码
|
||||
public String msg; // 描述信息
|
||||
public Object data; // 携带对象
|
||||
public Long dataCount; // 数据总数,用于分页
|
||||
|
||||
/**
|
||||
* 返回code
|
||||
* @return
|
||||
*/
|
||||
public int getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 给msg赋值,连缀风格
|
||||
*/
|
||||
public AjaxJson setMsg(String msg) {
|
||||
this.msg = msg;
|
||||
return this;
|
||||
}
|
||||
public String getMsg() {
|
||||
return this.msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 给data赋值,连缀风格
|
||||
*/
|
||||
public AjaxJson setData(Object data) {
|
||||
this.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将data还原为指定类型并返回
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getData(Class<T> cs) {
|
||||
return (T) data;
|
||||
}
|
||||
|
||||
// ============================ 构建 ==================================
|
||||
|
||||
public AjaxJson(int code, String msg, Object data, Long dataCount) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
this.dataCount = dataCount;
|
||||
}
|
||||
|
||||
// 返回成功
|
||||
public static AjaxJson getSuccess() {
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", null, null);
|
||||
}
|
||||
public static AjaxJson getSuccess(String msg) {
|
||||
return new AjaxJson(CODE_SUCCESS, msg, null, null);
|
||||
}
|
||||
public static AjaxJson getSuccess(String msg, Object data) {
|
||||
return new AjaxJson(CODE_SUCCESS, msg, data, null);
|
||||
}
|
||||
public static AjaxJson getSuccessData(Object data) {
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
|
||||
}
|
||||
public static AjaxJson getSuccessArray(Object... data) {
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
|
||||
}
|
||||
|
||||
// 返回失败
|
||||
public static AjaxJson getError() {
|
||||
return new AjaxJson(CODE_ERROR, "error", null, null);
|
||||
}
|
||||
public static AjaxJson getError(String msg) {
|
||||
return new AjaxJson(CODE_ERROR, msg, null, null);
|
||||
}
|
||||
|
||||
// 返回警告
|
||||
public static AjaxJson getWarning() {
|
||||
return new AjaxJson(CODE_ERROR, "warning", null, null);
|
||||
}
|
||||
public static AjaxJson getWarning(String msg) {
|
||||
return new AjaxJson(CODE_WARNING, msg, null, null);
|
||||
}
|
||||
|
||||
// 返回未登录
|
||||
public static AjaxJson getNotLogin() {
|
||||
return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
|
||||
}
|
||||
|
||||
// 返回没有权限的
|
||||
public static AjaxJson getNotJur(String msg) {
|
||||
return new AjaxJson(CODE_NOT_JUR, msg, null, null);
|
||||
}
|
||||
|
||||
// 返回一个自定义状态码的
|
||||
public static AjaxJson get(int code, String msg){
|
||||
return new AjaxJson(code, msg, null, null);
|
||||
}
|
||||
|
||||
// 返回分页和数据的
|
||||
public static AjaxJson getPageData(Long dataCount, Object data){
|
||||
return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
|
||||
}
|
||||
|
||||
// 返回,根据受影响行数的(大于0=ok,小于0=error)
|
||||
public static AjaxJson getByLine(int line){
|
||||
if(line > 0){
|
||||
return getSuccess("ok", line);
|
||||
}
|
||||
return getError("error").setData(line);
|
||||
}
|
||||
|
||||
// 返回,根据布尔值来确定最终结果的 (true=ok,false=error)
|
||||
public static AjaxJson getByBoolean(boolean b){
|
||||
return b ? getSuccess("ok") : getError("error");
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
public String toString() {
|
||||
String data_string = null;
|
||||
if(data == null){
|
||||
|
||||
} else if(data instanceof List){
|
||||
data_string = "List(length=" + ((List)data).size() + ")";
|
||||
} else {
|
||||
data_string = data.toString();
|
||||
}
|
||||
return "{"
|
||||
+ "\"code\": " + this.getCode()
|
||||
+ ", \"msg\": \"" + this.getMsg() + "\""
|
||||
+ ", \"data\": " + data_string
|
||||
+ ", \"dataCount\": " + dataCount
|
||||
+ "}";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -8,8 +8,9 @@ sa-token:
|
||||
sso:
|
||||
# Ticket有效期 (单位: 秒),默认五分钟
|
||||
ticket-timeout: 300
|
||||
# 所有允许的授权回调地址 (此处为了方便测试配置为*,线上生产环境一定要配置为详细地地址)
|
||||
allow-url: http://sa-sso-client1.com:9001/ssoLogin, http://sa-sso-client2.com:9001/ssoLogin, http://sa-sso-client3.com:9001/ssoLogin
|
||||
# 所有允许的授权回调地址
|
||||
# allow-url: http://sa-sso-client1.com:9001/sso/login, http://sa-sso-client2.com:9001/sso/login, http://sa-sso-client3.com:9001/sso/login
|
||||
allow-url: "*"
|
||||
|
||||
spring:
|
||||
# Redis配置
|
||||
@@ -22,8 +23,8 @@ spring:
|
||||
port: 6379
|
||||
# Redis服务器连接密码(默认为空)
|
||||
password:
|
||||
# 连接超时时间(毫秒)
|
||||
timeout: 10ms
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
pool:
|
||||
# 连接池最大连接数
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user