diff --git a/.workflow/branch-pipeline.yml b/.workflow/branch-pipeline.yml new file mode 100644 index 0000000000000000000000000000000000000000..9d2a2926f31b55f68083a3cb58c4cb6d6f43a07f --- /dev/null +++ b/.workflow/branch-pipeline.yml @@ -0,0 +1,53 @@ +version: '1.0' +name: branch-pipeline +displayName: BranchPipeline +stages: + - stage: + name: compile + displayName: 编译 + steps: + - step: build@maven + name: build_maven + displayName: Maven 构建 + # 支持6、7、8、9、10、11六个版本 + jdkVersion: 8 + # 支持2.2.1、3.2.5、3.3.9、3.5.2、3.5.3、3.5.4、3.6.1、3.6.3八个版本 + mavenVersion: 3.3.9 + # 构建命令 + commands: + - mvn -B clean package -Dmaven.test.skip=true + # 非必填字段,开启后表示将构建产物暂存,但不会上传到制品库中,7天后自动清除 + artifacts: + # 构建产物名字,作为产物的唯一标识可向下传递,支持自定义,默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址 + - name: BUILD_ARTIFACT + # 构建产物获取路径,是指代码编译完毕之后构建物的所在路径,如通常jar包在target目录下。当前目录为代码库根目录 + path: + - ./target + - step: publish@general_artifacts + name: publish_general_artifacts + displayName: 上传制品 + # 上游构建任务定义的产物名,默认BUILD_ARTIFACT + dependArtifact: BUILD_ARTIFACT + # 上传到制品库时的制品命名,默认output + artifactName: output + dependsOn: build_maven + - stage: + name: release + displayName: 发布 + steps: + - step: publish@release_artifacts + name: publish_release_artifacts + displayName: '发布' + # 上游上传制品任务的产出 + dependArtifact: output + # 发布制品版本号 + version: '1.0.0.0' + # 是否开启版本号自增,默认开启 + autoIncrement: true +triggers: + push: + branches: + exclude: + - master + include: + - .* diff --git a/.workflow/master-pipeline.yml b/.workflow/master-pipeline.yml new file mode 100644 index 0000000000000000000000000000000000000000..5d926c26f79e41d10427f87bf003ef99fe2fbd79 --- /dev/null +++ b/.workflow/master-pipeline.yml @@ -0,0 +1,51 @@ +version: '1.0' +name: master-pipeline +displayName: MasterPipeline +stages: + - stage: + name: compile + displayName: 编译 + steps: + - step: build@maven + name: build_maven + displayName: Maven 构建 + # 支持6、7、8、9、10、11六个版本 + jdkVersion: 8 + # 支持2.2.1、3.2.5、3.3.9、3.5.2、3.5.3、3.5.4、3.6.1、3.6.3八个版本 + mavenVersion: 3.3.9 + # 构建命令 + commands: + - mvn -B clean package -Dmaven.test.skip=true + # 非必填字段,开启后表示将构建产物暂存,但不会上传到制品库中,7天后自动清除 + artifacts: + # 构建产物名字,作为产物的唯一标识可向下传递,支持自定义,默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址 + - name: BUILD_ARTIFACT + # 构建产物获取路径,是指代码编译完毕之后构建物的所在路径,如通常jar包在target目录下。当前目录为代码库根目录 + path: + - ./target + - step: publish@general_artifacts + name: publish_general_artifacts + displayName: 上传制品 + # 上游构建任务定义的产物名,默认BUILD_ARTIFACT + dependArtifact: BUILD_ARTIFACT + # 上传到制品库时的制品命名,默认output + artifactName: output + dependsOn: build_maven + - stage: + name: release + displayName: 发布 + steps: + - step: publish@release_artifacts + name: publish_release_artifacts + displayName: '发布' + # 上游上传制品任务的产出 + dependArtifact: output + # 发布制品版本号 + version: '1.0.0.0' + # 是否开启版本号自增,默认开启 + autoIncrement: true +triggers: + push: + branches: + include: + - master diff --git a/.workflow/pr-pipeline.yml b/.workflow/pr-pipeline.yml new file mode 100644 index 0000000000000000000000000000000000000000..3f7579dd405c85f97f77df0357ec4893e616f85f --- /dev/null +++ b/.workflow/pr-pipeline.yml @@ -0,0 +1,40 @@ +version: '1.0' +name: pr-pipeline +displayName: PRPipeline +stages: + - stage: + name: compile + displayName: 编译 + steps: + - step: build@maven + name: build_maven + displayName: Maven 构建 + # 支持6、7、8、9、10、11六个版本 + jdkVersion: 8 + # 支持2.2.1、3.2.5、3.3.9、3.5.2、3.5.3、3.5.4、3.6.1、3.6.3八个版本 + mavenVersion: 3.3.9 + # 构建命令 + commands: + - mvn -B clean package -Dmaven.test.skip=true + # 非必填字段,开启后表示将构建产物暂存,但不会上传到制品库中,7天后自动清除 + artifacts: + # 构建产物名字,作为产物的唯一标识可向下传递,支持自定义,默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址 + - name: BUILD_ARTIFACT + # 构建产物获取路径,是指代码编译完毕之后构建物的所在路径,如通常jar包在target目录下。当前目录为代码库根目录 + path: + - ./target + - step: publish@general_artifacts + name: publish_general_artifacts + displayName: 上传制品 + # 上游构建任务定义的产物名,默认BUILD_ARTIFACT + dependArtifact: BUILD_ARTIFACT + # 构建产物制品库,默认default,系统默认创建 + artifactRepository: default + # 上传到制品库时的制品命名,默认output + artifactName: output + dependsOn: build_maven +triggers: + pr: + branches: + include: + - master diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000000000000000000000000000000000..122583568c41a7f19309eeddb9ebe07caf7d3eb2 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,22 @@ +# 这是一个注释。 +# 指定根目录下README文件的Code Owner +# commiter 非项目成员 维护者 维护者 开发者 +README.md @duxin @stars @yinlin @yanfan @gitcode_test + +# 指定scripts文件夹的Code Owner gitee规则 +scripts/* @yinlin + +# 指定scripts文件夹的Code Owner github规则 +/scripts/* @jiulongSQ + +# 指定scripts文件夹的Code Owner,不一定是根目录 gitee规则 +scripts/ @aron1 + +# 指定scripts文件夹的Code Owner,不一定是根目录 github规则 +/scripts/ @yanfan + +# 指定所有JS文件的Code Owner +*.js @stars + +# 指定docs文件夹的多个Code Owners +/docs/ @username1 @username2 \ No newline at end of file diff --git a/README.md b/README.md index f67bf8741418543afd4b2ff54463470b117559f5..406c22480f66e9e2b61b9d624dab63bd99c8b63b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ +

哈哈哈

logo

-

Sa-Token v1.37.0

+

Sa-Token v1.39.0

一个轻量级 Java 权限认证框架,让鉴权变得简单、优雅!

@@ -12,20 +13,20 @@

+ +

在线文档:https://sa-token.cc

---- -## 前言: -- [在线文档:https://sa-token.cc](https://sa-token.cc) +--- -- 注:学习测试请拉取 master 分支,dev 是开发分支,有很多特性并不稳定(在项目根目录执行 `git checkout master`)。 +### Sa-Token 介绍 -- 开源不易,点个 star 鼓励一下吧! +Sa-Token 是一个轻量级 Java 权限认证框架,目前拥有五大核心模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。 -## Sa-Token 介绍 -**Sa-Token** 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。 +
+简单示例展示:(点击展开 / 折叠) Sa-Token 旨在以简单、优雅的方式完成系统的权限认证部分,以登录认证为例,你只需要: @@ -79,62 +80,81 @@ registry.addInterceptor(new SaInterceptor(handler -> { 当你受够 Shiro、SpringSecurity 等框架的三拜九叩之后,你就会明白,相对于这些传统老牌框架,Sa-Token 的 API 设计是多么的简单、优雅! +
+ + +
+ 核心模块一览:(点击展开 / 折叠) + +- **登录认证** —— 单端登录、多端登录、同端互斥登录、七天内免登录。 +- **权限认证** —— 权限认证、角色认证、会话二级认证。 +- **踢人下线** —— 根据账号id踢人下线、根据Token值踢人下线。 +- **注解式鉴权** —— 优雅的将鉴权与业务代码分离。 +- **路由拦截式鉴权** —— 根据路由拦截鉴权,可适配 restful 模式。 +- **Session会话** —— 全端共享Session,单端独享Session,自定义Session,方便的存取值。 +- **持久层扩展** —— 可集成 Redis,重启数据不丢失。 +- **前后台分离** —— APP、小程序等不支持 Cookie 的终端也可以轻松鉴权。 +- **Token风格定制** —— 内置六种 Token 风格,还可:自定义 Token 生成策略。 +- **记住我模式** —— 适配 [记住我] 模式,重启浏览器免验证。 +- **二级认证** —— 在已登录的基础上再次认证,保证安全性。 +- **模拟他人账号** —— 实时操作任意用户状态数据。 +- **临时身份切换** —— 将会话身份临时切换为其它账号。 +- **同端互斥登录** —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录。 +- **账号封禁** —— 登录封禁、按照业务分类封禁、按照处罚阶梯封禁。 +- **密码加密** —— 提供基础加密算法,可快速 MD5、SHA1、SHA256、AES 加密。 +- **会话查询** —— 提供方便灵活的会话查询接口。 +- **Http Basic认证** —— 一行代码接入 Http Basic、Digest 认证。 +- **全局侦听器** —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作。 +- **全局过滤器** —— 方便的处理跨域,全局设置安全响应头等操作。 +- **多账号体系认证** —— 一个系统多套账号分开鉴权(比如商城的 User 表和 Admin 表) +- **单点登录** —— 内置三种单点登录模式:同域、跨域、同Redis、跨Redis、前后端分离等架构都可以搞定。 +- **单点注销** —— 任意子系统内发起注销,即可全端下线。 +- **OAuth2.0认证** —— 轻松搭建 OAuth2.0 服务,支持openid模式 。 +- **分布式会话** —— 提供共享数据中心分布式会话方案。 +- **微服务网关鉴权** —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证。 +- **RPC调用鉴权** —— 网关转发鉴权,RPC调用鉴权,让服务调用不再裸奔 +- **临时Token认证** —— 解决短时间的 Token 授权问题。 +- **独立Redis** —— 将权限缓存与业务缓存分离。 +- **Quick快速登录认证** —— 为项目零代码注入一个登录页面。 +- **标签方言** —— 提供 Thymeleaf 标签方言集成包,提供 beetl 集成示例。 +- **jwt集成** —— 提供三种模式的 jwt 集成方案,提供 token 扩展参数能力。 +- **RPC调用状态传递** —— 提供 dubbo、grpc 等集成包,在RPC调用时登录状态不丢失。 +- **参数签名** —— 提供跨系统API调用签名校验模块,防参数篡改,防请求重放。 +- **自动续签** —— 提供两种Token过期策略,灵活搭配使用,还可自动续签。 +- **开箱即用** —— 提供SpringMVC、WebFlux、Solon 等常见框架集成包,开箱即用。 +- **最新技术栈** —— 适配最新技术栈:支持 SpringBoot 3.x,jdk 17。 + +
-## Sa-Token 功能模块一览 - -Sa-Token 目前主要五大功能模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。 - -- **登录认证** —— 单端登录、多端登录、同端互斥登录、七天内免登录 -- **权限认证** —— 权限认证、角色认证、会话二级认证 -- **Session会话** —— 全端共享Session、单端独享Session、自定义Session -- **踢人下线** —— 根据账号id踢人下线、根据Token值踢人下线 -- **账号封禁** —— 登录封禁、按照业务分类封禁、按照处罚阶梯封禁 -- **持久层扩展** —— 可集成Redis、Memcached等专业缓存中间件,重启数据不丢失 -- **分布式会话** —— 提供jwt集成、共享数据中心两种分布式会话方案 -- **微服务网关鉴权** —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证 -- **单点登录** —— 内置三种单点登录模式:无论是否跨域、是否共享Redis,都可以搞定 -- **OAuth2.0认证** —— 轻松搭建 OAuth2.0 服务,支持openid模式 -- **二级认证** —— 在已登录的基础上再次认证,保证安全性 -- **Basic认证** —— 一行代码接入 Http Basic 认证 -- **独立Redis** —— 将权限缓存与业务缓存分离 -- **临时Token认证** —— 解决短时间的Token授权问题 -- **模拟他人账号** —— 实时操作任意用户状态数据 -- **临时身份切换** —— 将会话身份临时切换为其它账号 -- **前后台分离** —— APP、小程序等不支持Cookie的终端 -- **同端互斥登录** —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录 -- **多账号认证体系** —— 比如一个商城项目的user表和admin表分开鉴权 -- **Token风格定制** —— 内置六种Token风格,还可:自定义Token生成策略、自定义Token前缀 -- **注解式鉴权** —— 优雅的将鉴权与业务代码分离 -- **路由拦截式鉴权** —— 根据路由拦截鉴权,可适配restful模式 -- **自动续签** —— 提供两种Token过期策略,灵活搭配使用,还可自动续签 -- **会话治理** —— 提供方便灵活的会话查询接口 -- **记住我模式** —— 适配[记住我]模式,重启浏览器免验证 -- **密码加密** —— 提供密码加密模块,可快速MD5、SHA1、SHA256、AES、RSA加密 -- **全局侦听器** —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作 -- **开箱即用** —— 提供SpringMVC、WebFlux等常见web框架starter集成包,真正的开箱即用 - -功能结构图: - ![sa-token-js](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/x/sa-token-js4.png) -## Sa-Token-SSO 单点登录 -Sa-Token-SSO 由简入难划分为三种模式,解决不同架构下的 SSO 接入问题: +### SSO 单点登录 +Sa-Token SSO 分为三种模式,解决同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离……等不同架构下的 SSO 接入问题: | 系统架构 | 采用模式 | 简介 | 文档链接 | | :-------- | :-------- | :-------- | :-------- | -| 前端同域 + 后端同 Redis | 模式一 | 共享Cookie同步会话 | [文档](https://sa-token.cc/doc.html#/sso/sso-type1)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso1-client) | +| 前端同域 + 后端同 Redis | 模式一 | 共享Cookie同步会话 | [文档](https://sa-token.cc/doc.html#/sso/sso-type1)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso1-client) | | 前端不同域 + 后端同 Redis | 模式二 | URL重定向传播会话 | [文档](https://sa-token.cc/doc.html#/sso/sso-type2)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso2-client) | -| 前端不同域 + 后端 不同Redis | 模式三 | Http请求获取会话 | [文档](https://sa-token.cc/doc.html#/sso/sso-type3)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso3-client) | +| 前端不同域 + 后端 不同Redis | 模式三 | Http请求获取会话 | [文档](https://sa-token.cc/doc.html#/sso/sso-type3)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso3-client) | 1. 前端同域:就是指多个系统可以部署在同一个主域名之下,比如:`c1.domain.com`、`c2.domain.com`、`c3.domain.com` -2. 后端同Redis:就是指多个系统可以连接同一个Redis。PS:这里并不需要把所有项目的数据都放在同一个Redis中,Sa-Token提供了 **`[权限缓存与业务缓存分离]`** 的解决方案,详情戳:[Alone独立Redis插件](https://sa-token.cc/doc.html#/plugin/alone-redis) -3. 如果既无法做到前端同域,也无法做到后端同Redis,那么只能走模式三,Http请求获取会话(Sa-Token对SSO提供了完整的封装,你只需要按照示例从文档上复制几段代码便可以轻松集成) +2. 后端同Redis:就是指多个系统可以连接同一个Redis。(此处并非要所有项目数据都放在一个Redis中,Sa-Token提供 **`[权限缓存与业务缓存分离]`** 的解决方案) +3. 如果既无法做到前端同域,也无法做到后端同Redis,可以走模式三,Http请求校验 ticket 获取会话。 +4. 提供 NoSdk 模式示例,不使用 Sa-Token 的系统也可以对接。 +5. 提供 sso-server 接口文档,不使用 java 语言的系统也可以对接。 +6. 提供前后端分离整合方案:无论是 sso-server 还是 sso-client 的前后端分离都可以整合。 +7. 提供安全校验:域名校验、ticket校验、参数签名校验,有效防 ticket 劫持,防请求重放等攻击。 +8. 参数防丢:笔者曾试验多个SSO框架,均有参数丢失情况,比如登录前是:`http://a.com?id=1&name=2`,登录成功后就变成了:`http://a.com?id=1`,Sa-Token-SSO 内有专门算法保证了参数不丢失,登录成功后精准原路返回。 +9. 提供用户数据同步/迁移方案的建议:开发前统一迁移、运行时实时数据同步、根据关联字段匹配、根据 center_id 字段匹配等。 +10. 提供直接可运行的 demo 示例,帮助你快速熟悉 SSO 大致登录流程。 + + -## Sa-Token-OAuth2 授权认证 -Sa-OAuth2 模块分为四种授权模式,解决不同场景下的授权需求 +### OAuth2 授权认证 +Sa-Token-OAuth2 模块分为四种授权模式,解决不同场景下的授权需求 | 授权模式 | 简介 | | :-------- | :-------- | @@ -146,50 +166,47 @@ Sa-OAuth2 模块分为四种授权模式,解决不同场景下的授权需求 详细参考文档:[https://sa-token.cc/doc.html#/oauth2/readme](https://sa-token.cc/doc.html#/oauth2/readme) -## 使用 Sa-Token 的开源项目 +### 开源集成案例 - [[ Snowy ]](https://gitee.com/xiaonuobase/snowy):国内首个国密前后分离快速开发平台,采用 Vue3 + AntDesignVue3 + Vite + SpringBoot + Mp + HuTool + SaToken。 - - [[ RuoYi-Vue-Plus ]](https://gitee.com/dromara/RuoYi-Vue-Plus):重写RuoYi-Vue所有功能 集成 Sa-Token+Mybatis-Plus+Jackson+Xxl-Job+knife4j+Hutool+OSS 定期同步 - - [[ RuoYi-Cloud-Plus ]](https://gitee.com/dromara/RuoYi-Cloud-Plus):重写RuoYi-Cloud所有功能 整合 SpringCloudAlibaba Dubbo3.0 Sa-Token Mybatis-Plus MQ OSS ES Xxl-Job Docker 全方位升级 定期同步 - - [[ EasyAdmin ]](https://gitee.com/lakernote/easy-admin):一个基于SpringBoot2 + Sa-Token + Mybatis-Plus + Snakerflow + Layui 的后台管理系统,灵活多变可前后端分离,也可单体,内置代码生成器、权限管理、工作流引擎等 - - [[ YC-Framework ]](http://framework.youcongtech.com/):致力于打造一款优秀的分布式微服务解决方案。 - - [[ Pig-Satoken ]](https://gitee.com/wchenyang/cloud-satoken):重写 Pig 授权方式为 Sa-Token,其他代码不变。 -更多开源案例可参考:[Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token) +还有更多优秀开源案例无法逐一展示,请参考:[Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token) -## 友情链接 +### 友情链接 - [[ OkHttps ]](https://gitee.com/ejlchina-zhxu/okhttps):轻量级 http 通信框架,API无比优雅,支持 WebSocket、Stomp 协议 - - [[ Bean Searcher ]](https://github.com/ejlchina/bean-searcher):专注高级查询的只读 ORM,使一行代码实现复杂列表检索! - - [[ Jpom ]](https://gitee.com/dromara/Jpom):简而轻的低侵入式在线构建、自动部署、日常运维、项目监控软件。 - - [[ TLog ]](https://gitee.com/dromara/TLog):一个轻量级的分布式日志标记追踪神器。 - - [[ hippo4j ]](https://gitee.com/agentart/hippo4j):强大的动态线程池框架,附带监控报警功能。 - - [[ hertzbeat ]](https://gitee.com/dromara/hertzbeat):易用友好的开源实时监控告警系统,无需Agent,高性能集群,强大自定义监控能力。 - - [[ Solon ]](https://gitee.com/noear/solon):一个更现代感的应用开发框架:更快、更小、更自由。 +- [[ Chat2DB ]](https://github.com/chat2db/Chat2DB):一个AI驱动的数据库管理和BI工具,支持Mysql、pg、Oracle、Redis等22种数据库的管理。 + + +### 代码托管 +- Gitee:[https://gitee.com/dromara/sa-token](https://gitee.com/dromara/sa-token) +- GitHub:[https://github.com/dromara/sa-token](https://github.com/dromara/sa-token) +- GitCode:[https://gitcode.com/click33/sa-token](https://gitcode.com/click33/sa-token) -## 知识星球 - -## 交流群 -QQ交流群:685792424 [点击加入](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Y05Ld4125W92YSwZ0gA8e3RhG9Q4Vsfx&authKey=IomXuIuhP9g8G7l%2ByfkrRsS7i%2Fna0lIBpkTXxx%2BQEaz0NNEyJq00kgeiC4dUyNLS&noverify=0&group_code=685792424) +### 交流群 + + +QQ交流群:823181187 [点击加入](https://qm.qq.com/q/EBIJVZBVGE) 微信交流群: - - + + + (扫码添加微信,备注:sa-token,邀您加入群聊) diff --git a/mvn clean.bat b/mvn clean.bat index baf093d2bafe1597cb32986f248374febdf5bc54..3f051542d07311c21c9d1e090e70364950f6b79a 100644 --- a/mvn clean.bat +++ b/mvn clean.bat @@ -2,38 +2,55 @@ :: 整体clean call mvn clean + :: demo模块clean cd sa-token-demo cd sa-token-demo-alone-redis & call mvn clean & cd .. cd sa-token-demo-alone-redis-cluster & call mvn clean & cd .. +cd sa-token-demo-beetl & call mvn clean & cd .. +cd sa-token-demo-bom-import & call mvn clean & cd .. cd sa-token-demo-case & call mvn clean & cd .. cd sa-token-demo-grpc & call mvn clean & cd .. +cd sa-token-demo-hutool-timed-cache & call mvn clean & cd .. cd sa-token-demo-jwt & call mvn clean & cd .. cd sa-token-demo-quick-login & call mvn clean & cd .. cd sa-token-demo-solon & call mvn clean & cd .. +cd sa-token-demo-solon-redisson & call mvn clean & cd .. cd sa-token-demo-springboot & call mvn clean & cd .. cd sa-token-demo-springboot3-redis & call mvn clean & cd .. cd sa-token-demo-springboot-redis & call mvn clean & cd .. cd sa-token-demo-springboot-redisson & call mvn clean & cd .. +cd sa-token-demo-ssm & call mvn clean & cd .. cd sa-token-demo-test & call mvn clean & cd .. cd sa-token-demo-thymeleaf & call mvn clean & cd .. -cd sa-token-demo-beetl & call mvn clean & cd .. cd sa-token-demo-webflux & call mvn clean & cd .. cd sa-token-demo-webflux-springboot3 & call mvn clean & cd .. cd sa-token-demo-websocket & call mvn clean & cd .. cd sa-token-demo-websocket-spring & call mvn clean & cd .. -cd sa-token-demo-bom-import & call mvn clean & cd .. -cd sa-token-demo-hutool-timed-cache & call mvn clean & cd .. +cd sa-token-demo-dubbo +cd sa-token-demo-dubbo-consumer & call mvn clean & cd .. +cd sa-token-demo-dubbo-provider & call mvn clean & cd .. +cd sa-token-demo-dubbo3-consumer & call mvn clean & cd .. +cd sa-token-demo-dubbo3-provider & call mvn clean & cd .. +cd .. +cd sa-token-demo-oauth2 +cd sa-token-demo-oauth2-client & call mvn clean & cd .. +cd sa-token-demo-oauth2-server & call mvn clean & cd .. +cd .. +cd sa-token-demo-remember-me +cd sa-token-demo-remember-me-server & call mvn clean & cd .. +cd .. cd sa-token-demo-sso cd sa-token-demo-sso-server & call mvn clean & cd .. cd sa-token-demo-sso1-client & call mvn clean & cd .. cd sa-token-demo-sso2-client & call mvn clean & cd .. cd sa-token-demo-sso3-client & call mvn clean & cd .. +cd sa-token-demo-sso3-client-test2 & call mvn clean & cd .. cd sa-token-demo-sso3-client-nosdk & call mvn clean & cd .. cd .. @@ -44,31 +61,6 @@ cd sa-token-demo-sso3-client-solon & call mvn clean & cd .. cd sa-token-demo-sso-server-solon & call mvn clean & cd .. cd .. -cd sa-token-demo-oauth2 -cd sa-token-demo-oauth2-client & call mvn clean & cd .. -cd sa-token-demo-oauth2-server & call mvn clean & cd .. -cd .. - -:: cd sa-token-demo-cross -:: cd sa-token-demo-cross-header-server & call mvn clean & cd .. -:: cd sa-token-demo-cross-cookie-server & call mvn clean & cd .. -:: cd .. - -cd sa-token-demo-dubbo -cd sa-token-demo-dubbo-consumer & call mvn clean & cd .. -cd sa-token-demo-dubbo-provider & call mvn clean & cd .. -cd sa-token-demo-dubbo3-consumer & call mvn clean & cd .. -cd sa-token-demo-dubbo3-provider & call mvn clean & cd .. -cd .. - -cd sa-token-demo-remember-me -cd sa-token-demo-remember-me-server & call mvn clean & cd .. -cd .. - - - - - cd .. diff --git a/pom.xml b/pom.xml index 4e3a16dfa4f79829599da48f8f03ab5e17beafbc..cc13611300ec0dccde265fb92001d81e21c6c6d4 100644 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,7 @@ - 1.37.0 + 1.39.0 1.8 utf-8 utf-8 diff --git a/sa-token-bom/pom.xml b/sa-token-bom/pom.xml index 3eb7454da9f70059f9e5e6c306412e74091e54aa..74e4b510949453dc0518e09726e4b7385b6c3818 100644 --- a/sa-token-bom/pom.xml +++ b/sa-token-bom/pom.xml @@ -13,7 +13,7 @@ https://github.com/dromara/sa-token - 1.37.0 + 1.39.0 @@ -99,6 +99,11 @@ sa-token-dubbo ${revision} + + cn.dev33 + sa-token-dubbo3 + ${revision} + cn.dev33 sa-token-grpc @@ -139,6 +144,11 @@ sa-token-redisx ${revision} + + cn.dev33 + sa-token-hutool-timed-cache + ${revision} + cn.dev33 sa-token-dialect-thymeleaf diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckOr.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckOr.java index 921dd386ca3e9b73b96285ffe20b43e98167de60..5e2ce73969ac85bd553f569c9f0727a150bc58c3 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckOr.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckOr.java @@ -40,18 +40,18 @@ public @interface SaCheckOr { SaCheckLogin[] login() default {}; /** - * 设定 @SaCheckPermission,参考 {@link SaCheckPermission} + * 设定 @SaCheckRole,参考 {@link SaCheckRole} * * @return / */ - SaCheckPermission[] permission() default {}; + SaCheckRole[] role() default {}; /** - * 设定 @SaCheckRole,参考 {@link SaCheckRole} + * 设定 @SaCheckPermission,参考 {@link SaCheckPermission} * * @return / */ - SaCheckRole[] role() default {}; + SaCheckPermission[] permission() default {}; /** * 设定 @SaCheckSafe,参考 {@link SaCheckSafe} @@ -61,11 +61,18 @@ public @interface SaCheckOr { SaCheckSafe[] safe() default {}; /** - * 设定 @SaCheckBasic,参考 {@link SaCheckHttpBasic} + * 设定 @SaCheckHttpBasic,参考 {@link SaCheckHttpBasic} + * + * @return / + */ + SaCheckHttpBasic[] httpBasic() default {}; + + /** + * 设定 @SaCheckBasic,参考 {@link SaCheckHttpDigest} * * @return / */ - SaCheckHttpBasic[] basic() default {}; + SaCheckHttpDigest[] httpDigest() default {}; /** * 设定 @SaCheckDisable,参考 {@link SaCheckDisable} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaAnnotationHandlerInterface.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaAnnotationHandlerInterface.java new file mode 100644 index 0000000000000000000000000000000000000000..fd91d3968464995f3bd6681b11d7cc53b1772354 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaAnnotationHandlerInterface.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.annotation.handler; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +/** + * 所有注解处理器的父接口 + * + * @author click33 + * @since 2024/8/2 + */ +public interface SaAnnotationHandlerInterface { + + /** + * 获取所要处理的注解类型 + * @return / + */ + Class getHandlerAnnotationClass(); + + /** + * 所需要执行的校验方法 + * @param at 注解对象 + * @param method 被标注的注解的方法引用 + */ + @SuppressWarnings("unchecked") + default void check(Annotation at, Method method) { + checkMethod((T) at, method); + } + + /** + * 所需要执行的校验方法(转换类型后) + * @param at 注解对象 + * @param method 被标注的注解的方法引用 + */ + void checkMethod(T at, Method method); + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckDisableHandler.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckDisableHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..d1faa35de9363044fbf72f60c363510053abe036 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckDisableHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.annotation.handler; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.annotation.SaCheckDisable; +import cn.dev33.satoken.stp.StpLogic; + +import java.lang.reflect.Method; + +/** + * 注解 SaCheckDisable 的处理器 + * + * @author click33 + * @since 2024/8/2 + */ +public class SaCheckDisableHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaCheckDisable.class; + } + + @Override + public void checkMethod(SaCheckDisable at, Method method) { + _checkMethod(at.type(), at.value(), at.level()); + } + + public static void _checkMethod(String type, String[] value, int level) { + StpLogic stpLogic = SaManager.getStpLogic(type, false); + + Object loginId = stpLogic.getLoginId(); + for (String service : value) { + stpLogic.checkDisableLevel(loginId, service, level); + } + } + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckHttpBasicHandler.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckHttpBasicHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e15ce6b3e62b543306040c70a5f942e772e6226 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckHttpBasicHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.annotation.handler; + +import cn.dev33.satoken.annotation.SaCheckHttpBasic; +import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; + +import java.lang.reflect.Method; + +/** + * 注解 SaCheckHttpBasic 的处理器 + * + * @author click33 + * @since 2024/8/2 + */ +public class SaCheckHttpBasicHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaCheckHttpBasic.class; + } + + @Override + public void checkMethod(SaCheckHttpBasic at, Method method) { + _checkMethod(at.realm(), at.account()); + } + + public static void _checkMethod(String realm, String account) { + SaHttpBasicUtil.check(realm, account); + } + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckHttpDigestHandler.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckHttpDigestHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..1829ce811bfb3fad6cf4fd7e0af82e9cada2d17f --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckHttpDigestHandler.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.annotation.handler; + +import cn.dev33.satoken.annotation.SaCheckHttpDigest; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.httpauth.digest.SaHttpDigestUtil; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.lang.reflect.Method; + +/** + * 注解 SaCheckHttpDigest 的处理器 + * + * @author click33 + * @since 2024/8/2 + */ +public class SaCheckHttpDigestHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaCheckHttpDigest.class; + } + + @Override + public void checkMethod(SaCheckHttpDigest at, Method method) { + _checkMethod(at.username(), at.password(), at.realm(), at.value()); + } + + public static void _checkMethod(String username, String password, String realm, String value) { + // 如果配置了 value,则以 value 优先 + if(SaFoxUtil.isNotEmpty(value)){ + String[] arr = value.split(":"); + if(arr.length != 2){ + throw new SaTokenException("注解参数配置错误,格式应如:username:password"); + } + SaHttpDigestUtil.check(arr[0], arr[1]); + return; + } + + // 如果配置了 username,则分别获取参数 + if(SaFoxUtil.isNotEmpty(username)){ + SaHttpDigestUtil.check(username, password, realm); + return; + } + + // 都没有配置,则根据全局配置参数进行校验 + SaHttpDigestUtil.check(); + } + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckLoginHandler.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckLoginHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..d549763a4e26e4dc91cc1c6bc30d6370486b0ef2 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckLoginHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.annotation.handler; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.annotation.SaCheckLogin; +import cn.dev33.satoken.stp.StpLogic; + +import java.lang.reflect.Method; + +/** + * 注解 SaCheckLogin 的处理器 + * + * @author click33 + * @since 2024/8/2 + */ +public class SaCheckLoginHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaCheckLogin.class; + } + + @Override + public void checkMethod(SaCheckLogin at, Method method) { + _checkMethod(at.type()); + } + + public static void _checkMethod(String type) { + StpLogic stpLogic = SaManager.getStpLogic(type, false); + + stpLogic.checkLogin(); + } + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckOrHandler.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckOrHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..bb7b9c1dfb449ce1c7a77bf6471495687b190d70 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckOrHandler.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.annotation.handler; + +import cn.dev33.satoken.annotation.*; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 注解 SaCheckOr 的处理器 + * + * @author click33 + * @since 2024/8/2 + */ +public class SaCheckOrHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaCheckOr.class; + } + + @Override + public void checkMethod(SaCheckOr at, Method method) { + _checkMethod(at.login(), at.role(), at.permission(), at.safe(), at.httpBasic(), at.httpDigest(), at.disable(), method); + } + + public static void _checkMethod( + SaCheckLogin[] login, + SaCheckRole[] role, + SaCheckPermission[] permission, + SaCheckSafe[] safe, + SaCheckHttpBasic[] httpBasic, + SaCheckHttpDigest[] httpDigest, + SaCheckDisable[] disable, + Method method + ) { + // 先把所有注解塞到一个 list 里 + List annotationList = new ArrayList<>(); + annotationList.addAll(Arrays.asList(login)); + annotationList.addAll(Arrays.asList(role)); + annotationList.addAll(Arrays.asList(permission)); + annotationList.addAll(Arrays.asList(safe)); + annotationList.addAll(Arrays.asList(disable)); + annotationList.addAll(Arrays.asList(httpBasic)); + annotationList.addAll(Arrays.asList(httpDigest)); + + // 如果 atList 为空,说明 SaCheckOr 上不包含任何注解校验,我们直接跳过即可 + if(annotationList.isEmpty()) { + return; + } + + // 逐个开始校验 >>> + List errorList = new ArrayList<>(); + for (Annotation item : annotationList) { + try { + SaAnnotationStrategy.instance.annotationHandlerMap.get(item.annotationType()).check(item, method); + // 只要有一个校验通过,就可以直接返回了 + return; + } catch (SaTokenException e) { + errorList.add(e); + } + } + + // 执行至此,说明所有注解校验都通过不了,此时 errorList 里面会有多个异常,我们随便抛出一个即可 + throw errorList.get(0); + } + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckPermissionHandler.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..6232109846d9bf3b981ef1c4541a7d2192b377b7 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckPermissionHandler.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.annotation.handler; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaMode; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.lang.reflect.Method; + +/** + * 注解 SaCheckPermission 的处理器 + * + * @author click33 + * @since 2024/8/2 + */ +public class SaCheckPermissionHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaCheckPermission.class; + } + + @Override + public void checkMethod(SaCheckPermission at, Method method) { + _checkMethod(at.type(), at.value(), at.mode(), at.orRole()); + } + + public static void _checkMethod(String type, String[] value, SaMode mode, String[] orRole) { + StpLogic stpLogic = SaManager.getStpLogic(type, false); + + String[] permissionArray = value; + try { + if(mode == SaMode.AND) { + stpLogic.checkPermissionAnd(permissionArray); + } else { + stpLogic.checkPermissionOr(permissionArray); + } + } catch (NotPermissionException e) { + // 权限认证校验未通过,再开始角色认证校验 + for (String role : orRole) { + String[] rArr = SaFoxUtil.convertStringToArray(role); + // 某一项 role 认证通过,则可以提前退出了,代表通过 + if (stpLogic.hasRoleAnd(rArr)) { + return; + } + } + throw e; + } + } + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckRoleHandler.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckRoleHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..f5297a3df73a4ff47ff327ab73e6bc09f6bdc2c4 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckRoleHandler.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.annotation.handler; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.annotation.SaMode; +import cn.dev33.satoken.stp.StpLogic; + +import java.lang.reflect.Method; + +/** + * 注解 SaCheckRole 的处理器 + * + * @author click33 + * @since 2024/8/2 + */ +public class SaCheckRoleHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaCheckRole.class; + } + + @Override + public void checkMethod(SaCheckRole at, Method method) { + _checkMethod(at.type(), at.value(), at.mode()); + } + + public static void _checkMethod(String type, String[] value, SaMode mode) { + StpLogic stpLogic = SaManager.getStpLogic(type, false); + + String[] roleArray = value; + if(mode == SaMode.AND) { + stpLogic.checkRoleAnd(roleArray); + } else { + stpLogic.checkRoleOr(roleArray); + } + } + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckSafeHandler.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckSafeHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..c4052385861cfd3fa48d80994af7497d77a562d3 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckSafeHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.annotation.handler; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.annotation.SaCheckSafe; +import cn.dev33.satoken.stp.StpLogic; + +import java.lang.reflect.Method; + +/** + * 注解 SaCheckSafe 的处理器 + * + * @author click33 + * @since 2024/8/2 + */ +public class SaCheckSafeHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaCheckSafe.class; + } + + @Override + public void checkMethod(SaCheckSafe at, Method method) { + _checkMethod(at.type(), at.value()); + } + + public static void _checkMethod(String type, String value) { + StpLogic stpLogic = SaManager.getStpLogic(type, false); + + stpLogic.checkSafe(value); + } + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaIgnoreHandler.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaIgnoreHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..1fdef90479f6ee47036333fdacb22fc1495ba8cd --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaIgnoreHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.annotation.handler; + +import cn.dev33.satoken.annotation.SaIgnore; +import cn.dev33.satoken.router.SaRouter; + +import java.lang.reflect.Method; + +/** + * 注解 SaIgnore 的处理器 + * + * @author click33 + * @since 2024/8/2 + */ +public class SaIgnoreHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaIgnore.class; + } + + @Override + public void checkMethod(SaIgnore at, Method method) { + _checkMethod(); + } + + public static void _checkMethod() { + SaRouter.stop(); + } + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/basic/SaBasicTemplate.java b/sa-token-core/src/main/java/cn/dev33/satoken/basic/SaBasicTemplate.java deleted file mode 100644 index e899154bca90ef81262e4c5cd5dad8933d1c88e9..0000000000000000000000000000000000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/basic/SaBasicTemplate.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2020-2099 sa-token.cc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package cn.dev33.satoken.basic; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.context.SaHolder; -import cn.dev33.satoken.error.SaErrorCode; -import cn.dev33.satoken.exception.NotBasicAuthException; -import cn.dev33.satoken.secure.SaBase64Util; -import cn.dev33.satoken.util.SaFoxUtil; - -/** - *

已更换至包:cn.dev33.satoken.httpauth.basic

- *

已更换名称:SaHttpBasicTemplate

- * - * Sa-Token Http Basic 认证模块 - * - * @author click33 - * @since 1.26.0 - */ -@Deprecated -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().setCode(SaErrorCode.CODE_10311); - } - - /** - * 获取浏览器提交的 Basic 参数 (裁剪掉前缀并解码) - * @return 值 - */ - public String getAuthorizationValue() { - - // 获取前端提交的请求头 Authorization 参数 - String authorization = SaHolder.getRequest().getHeader("Authorization"); - - // 如果不是以 Basic 作为前缀,则视为无效 - if(authorization == null || ! authorization.startsWith("Basic ")) { - 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)) { - throwNotBasicAuthException(realm); - } - } - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/config/SaSignConfig.java b/sa-token-core/src/main/java/cn/dev33/satoken/config/SaSignConfig.java index 1e5cd72d9565fe0adb9539ebbf55296029e16ebd..995ab5232494582b3b1d56eb57b0339aa774bee2 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/config/SaSignConfig.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/config/SaSignConfig.java @@ -37,6 +37,17 @@ public class SaSignConfig { private long timestampDisparity = 1000 * 60 * 15; + public SaSignConfig() { + } + + /** + * 构造函数 + * @param secretKey 秘钥 + */ + public SaSignConfig(String secretKey) { + this.secretKey = secretKey; + } + /** * 获取 API 调用签名秘钥 * diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaRequest.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaRequest.java index 7c0c0fa9fbf06f65e1027388dc7a36bac2e8f91f..91c0f73f27c68eb499674ffb31fd908da969e6bc 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaRequest.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaRequest.java @@ -17,6 +17,7 @@ package cn.dev33.satoken.context.model; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.util.SaFoxUtil; import java.util.List; @@ -130,6 +131,20 @@ public interface SaRequest { */ String getCookieValue(String name); + /** + * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) + * @param name 键 + * @return 值 + */ + String getCookieFirstValue(String name); + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) + * @param name 键 + * @return 值 + */ + String getCookieLastValue(String name); + /** * 返回当前请求path (不包括上下文名称) * @return / @@ -156,7 +171,25 @@ public interface SaRequest { * @return / */ String getMethod(); - + + /** + * 返回当前请求 Method 是否为指定值 + * @param method method + * @return / + */ + default boolean isMethod(String method) { + return getMethod().equals(method); + } + + /** + * 返回当前请求 Method 是否为指定值 + * @param method method + * @return / + */ + default boolean isMethod(SaHttpMethod method) { + return getMethod().equals(method.name()); + } + /** * 判断此请求是否为 Ajax 异步请求 * @return / diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/error/SaErrorCode.java b/sa-token-core/src/main/java/cn/dev33/satoken/error/SaErrorCode.java index a6e8cd8b8d5abacc01c00593bde4f4f34b8a39d7..5449d9523551e99aad59cde6badc02544d611d5a 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/error/SaErrorCode.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/error/SaErrorCode.java @@ -125,6 +125,9 @@ public interface SaErrorCode { /** 获取 SaSession 时提供的 SessionId 为空 */ int CODE_11072 = 11072; + /** 获取 Token-Session 时提供的 token 为空 */ + int CODE_11073 = 11073; + // ------------ diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotBasicAuthException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpBasicAuthException.java similarity index 90% rename from sa-token-core/src/main/java/cn/dev33/satoken/exception/NotBasicAuthException.java rename to sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpBasicAuthException.java index 862c89862f0a4cbe7d7d08db626c1b3542260143..4a5defd90dc05cb89e00cce08e3ea3d0af670e9b 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotBasicAuthException.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpBasicAuthException.java @@ -21,7 +21,7 @@ package cn.dev33.satoken.exception; * @author click33 * @since 1.26.0 */ -public class NotBasicAuthException extends SaTokenException { +public class NotHttpBasicAuthException extends SaTokenException { /** * 序列化版本号 @@ -34,7 +34,7 @@ public class NotBasicAuthException extends SaTokenException { /** * 一个异常:代表会话未通过 Http Basic 认证 */ - public NotBasicAuthException() { + public NotHttpBasicAuthException() { super(BE_MESSAGE); } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaGetAnnotationFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaGetAnnotationFunction.java index 6908b63913928fc8accada00b3d3ed4628d2962b..6128263302356b71192c7248d5e6906872714228 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaGetAnnotationFunction.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaGetAnnotationFunction.java @@ -29,6 +29,6 @@ import java.util.function.BiFunction; * @since 1.35.0 */ @FunctionalInterface -public interface SaGetAnnotationFunction extends BiFunction , Annotation> { +public interface SaGetAnnotationFunction extends BiFunction, Annotation> { } \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicTemplate.java b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicTemplate.java index 4f7449bd8e9c717073afaf6d21411bab18a4b027..90f8553cf9046791a7dc74dc3520aa18c90f6a46 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicTemplate.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicTemplate.java @@ -18,7 +18,7 @@ package cn.dev33.satoken.httpauth.basic; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.error.SaErrorCode; -import cn.dev33.satoken.exception.NotBasicAuthException; +import cn.dev33.satoken.exception.NotHttpBasicAuthException; import cn.dev33.satoken.secure.SaBase64Util; import cn.dev33.satoken.util.SaFoxUtil; @@ -41,7 +41,7 @@ public class SaHttpBasicTemplate { */ public void throwNotBasicAuthException(String realm) { SaHolder.getResponse().setStatus(401).setHeader("WWW-Authenticate", "Basic Realm=" + realm); - throw new NotBasicAuthException().setCode(SaErrorCode.CODE_10311); + throw new NotHttpBasicAuthException().setCode(SaErrorCode.CODE_10311); } /** diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestTemplate.java b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestTemplate.java index f7ed69d3d4e4be82ccf8ad1c3fbada0720fdd716..a16d2bf545d583c21828b6f8146f8d74a4504a02 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestTemplate.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestTemplate.java @@ -263,11 +263,16 @@ public class SaHttpDigestTemplate { check(arr[0], arr[1]); } + + + // ----------------- 过期方法 ----------------- + /** * 根据注解 ( @SaCheckHttpDigest ) 鉴权 * * @param at 注解对象 */ + @Deprecated public void checkByAnnotation(SaCheckHttpDigest at) { // 如果配置了 value,则以 value 优先 diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestUtil.java index 70a0df46971b8ce6c2791c27ca13c5fa18ee541a..d3000554929efb8b7e73a179f7c921eee5e4c434 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestUtil.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestUtil.java @@ -90,11 +90,16 @@ public class SaHttpDigestUtil { saHttpDigestTemplate.check(); } + + + // ----------------- 过期方法 ----------------- + /** * 根据注解 ( @SaCheckHttpDigest ) 鉴权 * * @param at 注解对象 */ + @Deprecated public static void checkByAnnotation(SaCheckHttpDigest at) { saHttpDigestTemplate.checkByAnnotation(at); } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenEventCenter.java b/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenEventCenter.java index e5036e6407f9940288f48df2475673b40f837c86..53c18dd849f39f2a7f91e4fbf892535598b67f91 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenEventCenter.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenEventCenter.java @@ -18,6 +18,7 @@ package cn.dev33.satoken.listener; import java.util.ArrayList; import java.util.List; +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.SaTokenException; @@ -287,6 +288,16 @@ public class SaTokenEventCenter { } } + /** + * 事件发布:有新的注解处理器载入到框架中 + * @param handler 注解处理器 + */ + public static void doRegisterAnnotationHandler(SaAnnotationHandlerInterface handler) { + for (SaTokenListener listener : listenerList) { + listener.doRegisterAnnotationHandler(handler); + } + } + /** * 事件发布:有新的 StpLogic 载入到框架中 * @param stpLogic / diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListener.java b/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListener.java index 4de64e2e39befdec86862e42e7cadf7f24f14e25..1d867f2c5960299631f876dea43a31d23a777d9d 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListener.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListener.java @@ -15,6 +15,7 @@ */ package cn.dev33.satoken.listener; +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.stp.SaLoginModel; import cn.dev33.satoken.stp.StpLogic; @@ -125,6 +126,12 @@ public interface SaTokenListener { */ default void doRegisterComponent(String compName, Object compObj) {} + /** + * 注册了自定义注解处理器 + * @param handler 注解处理器 + */ + default void doRegisterAnnotationHandler(SaAnnotationHandlerInterface handler) {} + /** * StpLogic 对象替换 * @param stpLogic / diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForLog.java b/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForLog.java index 53a15b43f83c9fa3dc6c670a2b0ef87085e5e626..c1b61118cb39e0ff86ced46abe3f1964b655ed98 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForLog.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForLog.java @@ -15,13 +15,14 @@ */ package cn.dev33.satoken.listener; -import static cn.dev33.satoken.SaManager.log; - +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.stp.SaLoginModel; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.util.SaFoxUtil; +import static cn.dev33.satoken.SaManager.log; + /** * Sa-Token 侦听器的一个实现:Log 打印 * @@ -130,6 +131,17 @@ public class SaTokenListenerForLog implements SaTokenListener { log.info("全局组件 {} 载入成功: {}", compName, canonicalName); } + /** + * 注册了自定义注解处理器 + * @param handler 注解处理器 + */ + @Override + public void doRegisterAnnotationHandler(SaAnnotationHandlerInterface handler) { + if(handler != null) { + log.info("注解扩展 @{} (处理器: {})", handler.getHandlerAnnotationClass().getSimpleName(), handler.getClass().getCanonicalName()); + } + } + /** * StpLogic 对象替换 * @param stpLogic / diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/session/SaSession.java b/sa-token-core/src/main/java/cn/dev33/satoken/session/SaSession.java index 83d8bc2e73e9885c19ecd7afa491da371ab0e981..8e7f3e6f7fa480f16ae7e619043e52e47d124746 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/session/SaSession.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/session/SaSession.java @@ -96,7 +96,7 @@ public class SaSession implements SaSetValueInterface, Serializable { /** * 所有挂载数据 */ - private final Map dataMap = new ConcurrentHashMap<>(); + private Map dataMap = new ConcurrentHashMap<>(); // ----------------------- 构建相关 @@ -522,14 +522,26 @@ public class SaSession implements SaSetValueInterface, Serializable { return dataMap; } + /** + * 设置数据挂载集合 (改变底层对象引用,将 dataMap 整个对象替换) + * @param dataMap 数据集合 + * + * @return 对象自身 + */ + public SaSession setDataMap(Map dataMap) { + this.dataMap = dataMap; + return this; + } + /** * 写入数据集合 (不改变底层对象引用,只将此 dataMap 所有数据进行替换) * @param dataMap 数据集合 */ - public void refreshDataMap(Map dataMap) { + public SaSession refreshDataMap(Map dataMap) { this.dataMap.clear(); this.dataMap.putAll(dataMap); this.update(); + return this; } // diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/sign/SaSignTemplate.java b/sa-token-core/src/main/java/cn/dev33/satoken/sign/SaSignTemplate.java index 1076511db5ad34ba6ee3b843e74447fa235c4584..4ea4f9d48496eed7fa56635624d805a15dd55d2d 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/sign/SaSignTemplate.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/sign/SaSignTemplate.java @@ -26,6 +26,8 @@ import cn.dev33.satoken.util.SaFoxUtil; import java.util.Map; import java.util.TreeMap; +import static cn.dev33.satoken.SaManager.log; + /** * API 参数签名算法,在跨系统接口调用时防参数篡改、防重放攻击。 * @@ -42,6 +44,17 @@ import java.util.TreeMap; */ public class SaSignTemplate { + public SaSignTemplate() { + } + + /** + * 构造函数 + * @param signConfig 签名参数配置对象 + */ + public SaSignTemplate(SaSignConfig signConfig) { + this.signConfig = signConfig; + } + // ----------- 签名配置 SaSignConfig signConfig; @@ -160,7 +173,14 @@ public class SaSignTemplate { // 计算签名 String paramsStr = joinParamsDictSort(paramsMap); String fullStr = paramsStr + "&" + key + "=" + secretKey; - return abstractStr(fullStr); + String signStr = abstractStr(fullStr); + + // 输入日志,方便调试 + log.debug("fullStr:{}", fullStr); + log.debug("signStr:{}", signStr); + + // 返回 + return signStr; } /** diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/stp/SaLoginModel.java b/sa-token-core/src/main/java/cn/dev33/satoken/stp/SaLoginModel.java index b724837a8dee622c559ce6fd1955c8d59fef8e6c..db019fedc4bfd550069633135df5ff6f1dd38663 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/stp/SaLoginModel.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/stp/SaLoginModel.java @@ -300,6 +300,9 @@ public class SaLoginModel { if(getTimeoutOrGlobalConfig() == SaTokenDao.NEVER_EXPIRE) { return Integer.MAX_VALUE; } + if (timeout > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } return (int)(long)timeout; } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java b/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java index 971f5ad52e4abd70a85b99d68d5359f21abeb5b8..57cfc2c30b3f2b6ee107132b68319298f432b8a9 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java @@ -36,7 +36,7 @@ import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaTokenConsts; import cn.dev33.satoken.util.SaValue2Box; -import java.util.Collections; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -334,15 +334,15 @@ public class StpLogic { tokenValue = String.valueOf(storage.get(splicingKeyJustCreatedSave())); } // 2. 再尝试从 请求体 里面读取 - if(tokenValue == null && config.getIsReadBody()){ + if(SaFoxUtil.isEmpty(tokenValue) && config.getIsReadBody()){ tokenValue = request.getParam(keyTokenName); } // 3. 再尝试从 header 头里读取 - if(tokenValue == null && config.getIsReadHeader()){ + if(SaFoxUtil.isEmpty(tokenValue) && config.getIsReadHeader()){ tokenValue = request.getHeader(keyTokenName); } // 4. 最后尝试从 cookie 里读取 - if(tokenValue == null && config.getIsReadCookie()){ + if(SaFoxUtil.isEmpty(tokenValue) && config.getIsReadCookie()){ tokenValue = request.getCookieValue(keyTokenName); } @@ -590,6 +590,20 @@ public class StpLogic { } + /** + * 获取指定账号 id 的登录会话数据,如果获取不到则创建并返回 + * + * @param id 账号id,建议的类型:(long | int | String) + * @return 返回会话令牌 + */ + public String getOrCreateLoginSession(Object id) { + String tokenValue = getTokenValueByLoginId(id); + if(tokenValue == null) { + tokenValue = createLoginSession(id, new SaLoginModel()); + } + return tokenValue; + } + // --- 注销 /** @@ -994,12 +1008,12 @@ public class StpLogic { if(loginId == null) { return defaultValue; } - // 3、loginId 不为 null,则开始尝试类型转换 - if (defaultValue == null) { - return null; - } + // 3、loginId 不为 null,则开始尝试类型转换 + if(defaultValue == null) { + return (T) loginId; + } return (T) SaFoxUtil.getValueByType(loginId, defaultValue.getClass()); - } + } /** * 获取当前会话账号id, 如果未登录,则返回null @@ -1198,6 +1212,9 @@ public class StpLogic { // 如果是 Token-Session,则使用对用 token 的有效期,使 token 和 token-session 保持相同ttl,同步失效 if(SaTokenConsts.SESSION_TYPE__TOKEN.equals(session.getType())) { timeout = getTokenTimeout(session.getToken()); + if(timeout == SaTokenDao.NOT_VALUE_EXPIRE) { + timeout = getConfigOrGlobal().getTimeout(); + } } else { // 否则使用全局配置的 timeout timeout = getConfigOrGlobal().getTimeout(); @@ -1293,9 +1310,8 @@ public class StpLogic { */ public SaSession getTokenSessionByToken(String tokenValue, boolean isCreate) { if(SaFoxUtil.isEmpty(tokenValue)) { - throw new SaTokenException("Token-Session 获取失败:token 不能为空"); + throw new SaTokenException("Token-Session 获取失败:token 为空").setCode(SaErrorCode.CODE_11073); } - // todo 待优化 return getSessionBySessionId(splicingKeyTokenSession(tokenValue), isCreate, null, session -> { // 这里是该 Token-Session 首次创建时才会被执行的方法: // 设定这个 SaSession 的各种基础信息:类型、账号体系、Token 值 @@ -1331,7 +1347,7 @@ public class StpLogic { // 2、如果前端根本没有提供 Token ,则直接返回 null String tokenValue = getTokenValue(); if(SaFoxUtil.isEmpty(tokenValue)) { - return null; + throw new SaTokenException("Token-Session 获取失败:token 为空").setCode(SaErrorCode.CODE_11073); } // 3、代码至此:tokenSessionCheckLogin 校验通过、且 Token 有值 @@ -1408,7 +1424,14 @@ public class StpLogic { setTokenValue(tokenValue); // 返回其 Token-Session 对象 - return getTokenSessionByToken(tokenValue, isCreate); + final String finalTokenValue = tokenValue; + return getSessionBySessionId(splicingKeyTokenSession(tokenValue), isCreate, getConfigOrGlobal().getTimeout(), session -> { + // 这里是该 Anon-Token-Session 首次创建时才会被执行的方法: + // 设定这个 SaSession 的各种基础信息:类型、账号体系、Token 值 + session.setType(SaTokenConsts.SESSION_TYPE__TOKEN); + session.setLoginType(getLoginType()); + session.setToken(finalTokenValue); + }); } else { return null; @@ -2105,7 +2128,7 @@ public class StpLogic { // 如果该账号的 Account-Session 为 null,说明此账号尚没有客户端在登录,此时返回空集合 SaSession session = getSessionByLoginId(loginId, false); if(session == null) { - return Collections.emptyList(); + return new ArrayList<>(); } // 按照设备类型进行筛选 @@ -2123,7 +2146,7 @@ public class StpLogic { // 如果该账号的 Account-Session 为 null,说明此账号尚没有客户端在登录,此时返回空集合 SaSession session = getSessionByLoginId(loginId, false); if(session == null) { - return Collections.emptyList(); + return new ArrayList<>(); } // 按照设备类型进行筛选 @@ -2224,79 +2247,6 @@ public class StpLogic { public List searchTokenSessionId(String keyword, int start, int size, boolean sortType) { return getSaTokenDao().searchData(splicingKeyTokenSession(""), keyword, start, size, sortType); } - - - // ------------------- 注解鉴权 ------------------- - - /** - * 根据注解 ( @SaCheckLogin ) 鉴权 - * - * @param at 注解对象 - */ - public void checkByAnnotation(SaCheckLogin at) { - this.checkLogin(); - } - - /** - * 根据注解 ( @SaCheckRole ) 鉴权 - * - * @param at 注解对象 - */ - public void checkByAnnotation(SaCheckRole at) { - String[] roleArray = at.value(); - if(at.mode() == SaMode.AND) { - this.checkRoleAnd(roleArray); - } else { - this.checkRoleOr(roleArray); - } - } - - /** - * 根据注解 ( @SaCheckPermission ) 鉴权 - * - * @param at 注解对象 - */ - public void checkByAnnotation(SaCheckPermission at) { - String[] permissionArray = at.value(); - try { - if(at.mode() == SaMode.AND) { - this.checkPermissionAnd(permissionArray); - } else { - this.checkPermissionOr(permissionArray); - } - } catch (NotPermissionException e) { - // 权限认证校验未通过,再开始角色认证校验 - for (String role : at.orRole()) { - String[] rArr = SaFoxUtil.convertStringToArray(role); - // 某一项 role 认证通过,则可以提前退出了,代表通过 - if (hasRoleAnd(rArr)) { - return; - } - } - throw e; - } - } - - /** - * 根据注解 ( @SaCheckSafe ) 鉴权 - * - * @param at 注解对象 - */ - public void checkByAnnotation(SaCheckSafe at) { - this.checkSafe(at.value()); - } - - /** - * 根据注解 ( @SaCheckDisable ) 鉴权 - * - * @param at 注解对象 - */ - public void checkByAnnotation(SaCheckDisable at) { - Object loginId = getLoginId(); - for (String service : at.value()) { - this.checkDisableLevel(loginId, service, at.level()); - } - } // ------------------- 账号封禁 ------------------- @@ -2672,7 +2622,13 @@ public class StpLogic { return false; } - // 2、如果缓存中可以查询出指定的键值,则代表已认证,否则视为未认证 + // 2、如果此 token 不处于登录状态,也将其视为未认证 + Object loginId = getLoginIdNotHandle(tokenValue); + if( ! isValidLoginId(loginId) ) { + return false; + } + + // 3、如果缓存中可以查询出指定的键值,则代表已认证,否则视为未认证 String value = getSaTokenDao().get(splicingKeySafe(tokenValue, service)); return !(SaFoxUtil.isEmpty(value)); } @@ -2690,8 +2646,14 @@ public class StpLogic { * @param service 业务标识 */ public void checkSafe(String service) { + // 1、必须先通过登录校验 + checkLogin(); + + // 2、再进行二级认证校验 + // 如果缓存中可以查询出指定的键值,则代表已认证,否则视为未认证 String tokenValue = getTokenValue(); - if ( ! isSafe(tokenValue, service)) { + String value = getSaTokenDao().get(splicingKeySafe(tokenValue, service)); + if(SaFoxUtil.isEmpty(value)) { throw new NotSafeException(loginType, tokenValue, service).setCode(SaErrorCode.CODE_11071); } } @@ -2916,4 +2878,84 @@ public class StpLogic { return false; } + + + // ------------------- 过期方法 ------------------- + + /** + * 根据注解 ( @SaCheckLogin ) 鉴权 + * + * @param at 注解对象 + */ + @Deprecated + public void checkByAnnotation(SaCheckLogin at) { + this.checkLogin(); + } + + /** + * 根据注解 ( @SaCheckRole ) 鉴权 + * + * @param at 注解对象 + */ + @Deprecated + public void checkByAnnotation(SaCheckRole at) { + String[] roleArray = at.value(); + if(at.mode() == SaMode.AND) { + this.checkRoleAnd(roleArray); + } else { + this.checkRoleOr(roleArray); + } + } + + /** + * 根据注解 ( @SaCheckPermission ) 鉴权 + * + * @param at 注解对象 + */ + @Deprecated + public void checkByAnnotation(SaCheckPermission at) { + String[] permissionArray = at.value(); + try { + if(at.mode() == SaMode.AND) { + this.checkPermissionAnd(permissionArray); + } else { + this.checkPermissionOr(permissionArray); + } + } catch (NotPermissionException e) { + // 权限认证校验未通过,再开始角色认证校验 + for (String role : at.orRole()) { + String[] rArr = SaFoxUtil.convertStringToArray(role); + // 某一项 role 认证通过,则可以提前退出了,代表通过 + if (hasRoleAnd(rArr)) { + return; + } + } + throw e; + } + } + + /** + * 根据注解 ( @SaCheckSafe ) 鉴权 + * + * @param at 注解对象 + */ + @Deprecated + public void checkByAnnotation(SaCheckSafe at) { + this.checkSafe(at.value()); + } + + /** + * 根据注解 ( @SaCheckDisable ) 鉴权 + * + * @param at 注解对象 + */ + @Deprecated + public void checkByAnnotation(SaCheckDisable at) { + Object loginId = getLoginId(); + for (String service : at.value()) { + this.checkDisableLevel(loginId, service, at.level()); + } + } + + } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpUtil.java index aaf5ec7aedcbe24b326c2caf447287c994d185d8..60cf4b92b6be5810783e8aa77b64ed991f372948 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpUtil.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpUtil.java @@ -224,7 +224,17 @@ public class StpUtil { public static String createLoginSession(Object id, SaLoginModel loginModel) { return stpLogic.createLoginSession(id, loginModel); } - + + /** + * 获取指定账号 id 的登录会话数据,如果获取不到则创建并返回 + * + * @param id 账号id,建议的类型:(long | int | String) + * @return 返回会话令牌 + */ + public static String getOrCreateLoginSession(Object id) { + return stpLogic.getOrCreateLoginSession(id); + } + // --- 注销 /** diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaAnnotationStrategy.java b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaAnnotationStrategy.java new file mode 100644 index 0000000000000000000000000000000000000000..e0ffc23ae592f679a9f9cd5e8245cedd263f9a1e --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaAnnotationStrategy.java @@ -0,0 +1,133 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.strategy; + +import cn.dev33.satoken.annotation.*; +import cn.dev33.satoken.annotation.handler.*; +import cn.dev33.satoken.fun.strategy.SaCheckMethodAnnotationFunction; +import cn.dev33.satoken.fun.strategy.SaGetAnnotationFunction; +import cn.dev33.satoken.fun.strategy.SaIsAnnotationPresentFunction; +import cn.dev33.satoken.listener.SaTokenEventCenter; + +import java.lang.annotation.Annotation; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Sa-Token 注解鉴权相关策略 + * + * @author click33 + * @since 1.39.0 + */ +public final class SaAnnotationStrategy { + + private SaAnnotationStrategy() { + registerDefaultAnnotationHandler(); + } + + /** + * 全局单例引用 + */ + public static final SaAnnotationStrategy instance = new SaAnnotationStrategy(); + + + // ----------------------- 所有策略 + + /** + * 注解处理器集合 + */ + public Map, SaAnnotationHandlerInterface> annotationHandlerMap = new LinkedHashMap<>(); + + /** + * 注册所有默认的注解处理器 + */ + public void registerDefaultAnnotationHandler() { + annotationHandlerMap.put(SaIgnore.class, new SaIgnoreHandler()); + annotationHandlerMap.put(SaCheckLogin.class, new SaCheckLoginHandler()); + annotationHandlerMap.put(SaCheckRole.class, new SaCheckRoleHandler()); + annotationHandlerMap.put(SaCheckPermission.class, new SaCheckPermissionHandler()); + annotationHandlerMap.put(SaCheckSafe.class, new SaCheckSafeHandler()); + annotationHandlerMap.put(SaCheckDisable.class, new SaCheckDisableHandler()); + annotationHandlerMap.put(SaCheckHttpBasic.class, new SaCheckHttpBasicHandler()); + annotationHandlerMap.put(SaCheckHttpDigest.class, new SaCheckHttpDigestHandler()); + annotationHandlerMap.put(SaCheckOr.class, new SaCheckOrHandler()); + } + + /** + * 注册一个注解处理器 + */ + public void registerAnnotationHandler(SaAnnotationHandlerInterface handler) { + annotationHandlerMap.put(handler.getHandlerAnnotationClass(), handler); + SaTokenEventCenter.doRegisterAnnotationHandler(handler); + } + + /** + * 注册一个注解处理器,到首位 + */ + public void registerAnnotationHandlerToFirst(SaAnnotationHandlerInterface handler) { + Map, SaAnnotationHandlerInterface> newMap = new LinkedHashMap<>(); + newMap.put(handler.getHandlerAnnotationClass(), handler); + newMap.putAll(annotationHandlerMap); + this.annotationHandlerMap = newMap; + SaTokenEventCenter.doRegisterAnnotationHandler(handler); + } + + /** + * 移除一个注解处理器 + */ + public void removeAnnotationHandler(Class cls) { + annotationHandlerMap.remove(cls); + } + + /** + * 对一个 [Method] 对象进行注解校验 (注解鉴权内部实现) + */ + @SuppressWarnings("unchecked") + public SaCheckMethodAnnotationFunction checkMethodAnnotation = (method) -> { + // 遍历所有的注解处理器,检查此 method 是否具有这些指定的注解 + for (Map.Entry, SaAnnotationHandlerInterface> entry: annotationHandlerMap.entrySet()) { + + // 先校验 Method 所属 Class 上的注解 + Annotation classTakeAnnotation = instance.getAnnotation.apply(method.getDeclaringClass(), (Class)entry.getKey()); + if(classTakeAnnotation != null) { + entry.getValue().check(classTakeAnnotation, method); + } + + // 再校验 Method 上的注解 + Annotation methodTakeAnnotation = instance.getAnnotation.apply(method, (Class)entry.getKey()); + if(methodTakeAnnotation != null) { + entry.getValue().check(methodTakeAnnotation, method); + } + } + }; + + /** + * 从元素上获取注解 + */ + public SaGetAnnotationFunction getAnnotation = (element, annotationClass)->{ + // 默认使用jdk的注解处理器 + return element.getAnnotation(annotationClass); + }; + + /** + * 判断一个 Method 或其所属 Class 是否包含指定注解 + */ + public SaIsAnnotationPresentFunction isAnnotationPresent = (method, annotationClass) -> { + return instance.getAnnotation.apply(method, annotationClass) != null || + instance.getAnnotation.apply(method.getDeclaringClass(), annotationClass) != null; + }; + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java index cb1e47c2416d2ecf1a4b5379c076b050fb50ce09..3e12c8dd52a3b2b236ae11acfb841d23f8f0f447 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java @@ -16,19 +16,14 @@ package cn.dev33.satoken.strategy; import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.annotation.*; -import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; import cn.dev33.satoken.exception.RequestPathInvalidException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.fun.strategy.*; -import cn.dev33.satoken.httpauth.digest.SaHttpDigestUtil; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaTokenConsts; -import java.util.ArrayList; -import java.util.List; import java.util.UUID; /** @@ -133,174 +128,6 @@ public final class SaStrategy { return false; }; - /** - * 对一个 [Method] 对象进行注解校验 (注解鉴权内部实现) - */ - public SaCheckMethodAnnotationFunction checkMethodAnnotation = (method) -> { - - // 先校验 Method 所属 Class 上的注解 - instance.checkElementAnnotation.accept(method.getDeclaringClass()); - - // 再校验 Method 上的注解 - instance.checkElementAnnotation.accept(method); - }; - - /** - * 对一个 [元素] 对象进行注解校验 (注解鉴权内部实现) - */ - public SaCheckElementAnnotationFunction checkElementAnnotation = (element) -> { - - // 校验 @SaCheckLogin 注解 - SaCheckLogin checkLogin = (SaCheckLogin) SaStrategy.instance.getAnnotation.apply(element, SaCheckLogin.class); - if(checkLogin != null) { - SaManager.getStpLogic(checkLogin.type(), false).checkByAnnotation(checkLogin); - } - - // 校验 @SaCheckRole 注解 - SaCheckRole checkRole = (SaCheckRole) SaStrategy.instance.getAnnotation.apply(element, SaCheckRole.class); - if(checkRole != null) { - SaManager.getStpLogic(checkRole.type(), false).checkByAnnotation(checkRole); - } - - // 校验 @SaCheckPermission 注解 - SaCheckPermission checkPermission = (SaCheckPermission) SaStrategy.instance.getAnnotation.apply(element, SaCheckPermission.class); - if(checkPermission != null) { - SaManager.getStpLogic(checkPermission.type(), false).checkByAnnotation(checkPermission); - } - - // 校验 @SaCheckSafe 注解 - SaCheckSafe checkSafe = (SaCheckSafe) SaStrategy.instance.getAnnotation.apply(element, SaCheckSafe.class); - if(checkSafe != null) { - SaManager.getStpLogic(checkSafe.type(), false).checkByAnnotation(checkSafe); - } - - // 校验 @SaCheckDisable 注解 - SaCheckDisable checkDisable = (SaCheckDisable) SaStrategy.instance.getAnnotation.apply(element, SaCheckDisable.class); - if(checkDisable != null) { - SaManager.getStpLogic(checkDisable.type(), false).checkByAnnotation(checkDisable); - } - - // 校验 @SaCheckHttpBasic 注解 - SaCheckHttpBasic checkHttpBasic = (SaCheckHttpBasic) SaStrategy.instance.getAnnotation.apply(element, SaCheckHttpBasic.class); - if(checkHttpBasic != null) { - SaHttpBasicUtil.check(checkHttpBasic.realm(), checkHttpBasic.account()); - } - - // 校验 @SaCheckHttpDigest 注解 - SaCheckHttpDigest checkHttpDigest = (SaCheckHttpDigest) SaStrategy.instance.getAnnotation.apply(element, SaCheckHttpDigest.class); - if(checkHttpDigest != null) { - SaHttpDigestUtil.checkByAnnotation(checkHttpDigest); - } - - // 校验 @SaCheckOr 注解 - SaCheckOr checkOr = (SaCheckOr) SaStrategy.instance.getAnnotation.apply(element, SaCheckOr.class); - if(checkOr != null) { - SaStrategy.instance.checkOrAnnotation.accept(checkOr); - } - }; - - /** - * 对一个 @SaCheckOr 进行注解校验 - */ - public SaCheckOrAnnotationFunction checkOrAnnotation = (at) -> { - - // 记录校验过程中所有的异常 - List errorList = new ArrayList<>(); - - // 逐个开始校验 >>> - - // 1、校验注解:@SaCheckLogin - SaCheckLogin[] checkLoginArray = at.login(); - for (SaCheckLogin item : checkLoginArray) { - try { - SaManager.getStpLogic(item.type(), false).checkByAnnotation(item); - return; - } catch (SaTokenException e) { - errorList.add(e); - } - } - - // 2、校验注解:@SaCheckRole - SaCheckRole[] checkRoleArray = at.role(); - for (SaCheckRole item : checkRoleArray) { - try { - SaManager.getStpLogic(item.type(), false).checkByAnnotation(item); - return; - } catch (SaTokenException e) { - errorList.add(e); - } - } - - // 3、校验注解:@SaCheckPermission - SaCheckPermission[] checkPermissionArray = at.permission(); - for (SaCheckPermission item : checkPermissionArray) { - try { - SaManager.getStpLogic(item.type(), false).checkByAnnotation(item); - return; - } catch (SaTokenException e) { - errorList.add(e); - } - } - - // 4、校验注解:@SaCheckSafe - SaCheckSafe[] checkSafeArray = at.safe(); - for (SaCheckSafe item : checkSafeArray) { - try { - SaManager.getStpLogic(item.type(), false).checkByAnnotation(item); - return; - } catch (SaTokenException e) { - errorList.add(e); - } - } - - // 5、校验注解:@SaCheckDisable - SaCheckDisable[] checkDisableArray = at.disable(); - for (SaCheckDisable item : checkDisableArray) { - try { - SaManager.getStpLogic(item.type(), false).checkByAnnotation(item); - return; - } catch (SaTokenException e) { - errorList.add(e); - } - } - - // 6、校验注解:@SaCheckBasic - SaCheckHttpBasic[] checkBasicArray = at.basic(); - for (SaCheckHttpBasic item : checkBasicArray) { - try { - SaHttpBasicUtil.check(item.realm(), item.account()); - return; - } catch (SaTokenException e) { - errorList.add(e); - } - } - - // 如果执行到这里,有两种可能: - // 可能 1. SaCheckOr 注解上不包含任何注解校验,此时 errorList 里面一个异常都没有,我们直接跳过即可 - // 可能 2. 所有注解校验都通过不了,此时 errorList 里面会有多个异常,我们随便抛出一个即可 - if(errorList.size() == 0) { - // return; - } else { - throw errorList.get(0); - } - }; - - /** - * 从元素上获取注解 - */ - public SaGetAnnotationFunction getAnnotation = (element, annotationClass)->{ - // 默认使用jdk的注解处理器 - return element.getAnnotation(annotationClass); - }; - - /** - * 判断一个 Method 或其所属 Class 是否包含指定注解 - */ - public SaIsAnnotationPresentFunction isAnnotationPresent = (method, annotationClass) -> { - return instance.getAnnotation.apply(method, annotationClass) != null || - instance.getAnnotation.apply(method.getDeclaringClass(), annotationClass) != null; - }; - /** * 生成唯一式 token 的算法 */ @@ -416,62 +243,6 @@ public final class SaStrategy { return this; } - /** - * 对一个 [Method] 对象进行注解校验 (注解鉴权内部实现) - * - * @param checkMethodAnnotation / - * @return / - */ - public SaStrategy setCheckMethodAnnotation(SaCheckMethodAnnotationFunction checkMethodAnnotation) { - this.checkMethodAnnotation = checkMethodAnnotation; - return this; - } - - /** - * 对一个 [元素] 对象进行注解校验 (注解鉴权内部实现) - * - * @param checkElementAnnotation / - * @return / - */ - public SaStrategy setCheckElementAnnotation(SaCheckElementAnnotationFunction checkElementAnnotation) { - this.checkElementAnnotation = checkElementAnnotation; - return this; - } - - /** - * 对一个 @SaCheckOr 进行注解校验 - *

参数 [SaCheckOr 注解的实例] - * - * @param checkOrAnnotation / - * @return / - */ - public SaStrategy setCheckOrAnnotation(SaCheckOrAnnotationFunction checkOrAnnotation) { - this.checkOrAnnotation = checkOrAnnotation; - return this; - } - - /** - * 从元素上获取注解 - * - * @param getAnnotation / - * @return / - */ - public SaStrategy setGetAnnotation(SaGetAnnotationFunction getAnnotation) { - this.getAnnotation = getAnnotation; - return this; - } - - /** - * 判断一个 Method 或其所属 Class 是否包含指定注解 - * - * @param isAnnotationPresent / - * @return / - */ - public SaStrategy setIsAnnotationPresent(SaIsAnnotationPresentFunction isAnnotationPresent) { - this.isAnnotationPresent = isAnnotationPresent; - return this; - } - /** * 生成唯一式 token 的算法 * diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java index a28a1072c2eab3528c56e3dd6310b25ffaa03b42..63bfbb60808b97639b7f062db6a7b1c0972d4a9c 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java @@ -29,7 +29,6 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.ThreadLocalRandom; -import java.util.regex.Pattern; /** * Sa-Token 内部工具类 @@ -76,6 +75,17 @@ public class SaFoxUtil { return sb.toString(); } + /** + * 生成指定区间的 int 值 + * + * @param min 最小值(包括) + * @param max 最大值(包括) + * @return / + */ + public static int getRandomNumber(int min, int max) { + return ThreadLocalRandom.current().nextInt(min, max + 1); + } + /** * 指定元素是否为null或者空字符串 * @param str 指定元素 @@ -96,14 +106,35 @@ public class SaFoxUtil { /** * 指定数组是否为null或者空数组 + *

该方法已过时,建议使用 isEmptyArray 方法

* @param / * @param array / * @return / */ + @Deprecated public static boolean isEmpty(T[] array) { + return isEmptyArray(array); + } + + /** + * 指定数组是否为null或者空数组 + * @param / + * @param array / + * @return / + */ + public static boolean isEmptyArray(T[] array) { return array == null || array.length == 0; } + /** + * 指定集合是否为null或者空数组 + * @param list / + * @return / + */ + public static boolean isEmptyList(List list) { + return list == null || list.isEmpty(); + } + /** * 比较两个对象是否相等 * @param a 第一个对象 @@ -563,7 +594,7 @@ public class SaFoxUtil { * @return 字符串 */ public static String convertListToString(List list) { - if(list == null || list.size() == 0) { + if(list == null || list.isEmpty()) { return ""; } StringBuilder str = new StringBuilder(); @@ -616,6 +647,15 @@ public class SaFoxUtil { return new ArrayList<>(Arrays.asList(str)); } + /** + * String 集合转数组 + * @param list 集合 + * @return 数组 + */ + public static String[] toArray(List list) { + return list.toArray(new String[0]); + } + public static List logLevelList = Arrays.asList("", "trace", "debug", "info", "warn", "error", "fatal"); /** @@ -679,4 +719,63 @@ public class SaFoxUtil { return false; } + /** + * list1 是否完全包含 list2 中所有元素 + * @param list1 集合1 + * @param list2 集合2 + * @return / + */ + public static boolean list1ContainList2AllElement(List list1, List list2){ + if(list2 == null || list2.isEmpty()) { + return true; + } + if(list1 == null || list1.isEmpty()) { + return false; + } + for (String str : list2) { + if(!list1.contains(str)) { + return false; + } + } + return true; + } + + /** + * list1 是否包含 list2 中任意一个元素 + * @param list1 集合1 + * @param list2 集合2 + * @return / + */ + public static boolean list1ContainList2AnyElement(List list1, List list2){ + if(list1 == null || list1.isEmpty() || list2 == null || list2.isEmpty()) { + return false; + } + for (String str : list2) { + if(list1.contains(str)) { + return true; + } + } + return false; + } + + /** + * 从 list1 中剔除 list2 所包含的元素 (克隆副本操作,不影响 list1) + * @param list1 集合1 + * @param list2 集合2 + * @return / + */ + public static List list1RemoveByList2(List list1, List list2){ + if(list1 == null) { + return null; + } + if(list1.isEmpty() || list2 == null || list2.isEmpty()) { + return new ArrayList<>(list1); + } + List listX = new ArrayList<>(list1); + for (String str : list2) { + listX.remove(str); + } + return listX; + } + } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaResult.java b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaResult.java index 1cf36e2fe4a119aabc7184ff138dfffecf665163..4d70c9803785b0096d13c1941848bc5e458c53f4 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaResult.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaResult.java @@ -15,6 +15,8 @@ */ package cn.dev33.satoken.util; +import cn.dev33.satoken.SaManager; + import java.io.Serializable; import java.util.LinkedHashMap; import java.util.Map; @@ -150,7 +152,42 @@ public class SaResult extends LinkedHashMap implements Serializa } return this; } - + + /** + * 写入一个 json 字符串, 连缀风格 + * @param jsonString json 字符串 + * @return 对象自身 + */ + public SaResult setJsonString(String jsonString) { + Map map = SaManager.getSaJsonTemplate().parseJsonToMap(jsonString); + return setMap(map); + } + + /** + * 移除默认属性(code、msg、data), 连缀风格 + * @return 对象自身 + */ + public SaResult removeDefaultFields() { + this.remove("code"); + this.remove("msg"); + this.remove("data"); + return this; + } + + /** + * 移除非默认属性(code、msg、data), 连缀风格 + * @return 对象自身 + */ + public SaResult removeNonDefaultFields() { + for (String key : this.keySet()) { + if("code".equals(key) || "msg".equals(key) || "data".equals(key)) { + continue; + } + this.remove(key); + } + return this; + } + // ============================ 静态方法快速构建 ================================== @@ -180,7 +217,11 @@ public class SaResult extends LinkedHashMap implements Serializa public static SaResult get(int code, String msg, Object data) { return new SaResult(code, msg, data); } - + + // 构建一个空的 + public static SaResult empty() { + return new SaResult(); + } /* (non-Javadoc) * @see java.lang.Object#toString() diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenConsts.java b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenConsts.java index 05be26a099d304375cbcf6fad6e886de3e9b36ed..c54c2722b6cb10079af36871f09712fdd3ec2c49 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenConsts.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenConsts.java @@ -36,7 +36,7 @@ public class SaTokenConsts { /** * Sa-Token 当前版本号 */ - public static final String VERSION_NO = "v1.37.0"; + public static final String VERSION_NO = "v1.39.0"; /** * Sa-Token 开源地址 Gitee diff --git a/sa-token-demo/sa-token-demo-alone-redis-cluster/pom.xml b/sa-token-demo/sa-token-demo-alone-redis-cluster/pom.xml index 0981fe027153ad85a5e88124d1341e5936153204..b1835aaf9447045bc71f01f10ce6a459431f2a1e 100644 --- a/sa-token-demo/sa-token-demo-alone-redis-cluster/pom.xml +++ b/sa-token-demo/sa-token-demo-alone-redis-cluster/pom.xml @@ -16,7 +16,7 @@ - 1.37.0 + 1.39.0 diff --git a/sa-token-demo/sa-token-demo-alone-redis/pom.xml b/sa-token-demo/sa-token-demo-alone-redis/pom.xml index 21b48f8918e20770c1fa290406abf4e7dc3f7e7b..c036ca14f3236999a8283019d1fbb806d19df1f0 100644 --- a/sa-token-demo/sa-token-demo-alone-redis/pom.xml +++ b/sa-token-demo/sa-token-demo-alone-redis/pom.xml @@ -17,7 +17,7 @@ - 1.37.0 + 1.39.0 diff --git a/sa-token-demo/sa-token-demo-beetl/pom.xml b/sa-token-demo/sa-token-demo-beetl/pom.xml index e5d3de42048bd7e1745a426be6afc4ef909ec3c9..aa20837525585b137b2f0814ee6e621816fc3cd2 100644 --- a/sa-token-demo/sa-token-demo-beetl/pom.xml +++ b/sa-token-demo/sa-token-demo-beetl/pom.xml @@ -16,7 +16,7 @@ - 1.37.0 + 1.39.0 diff --git a/sa-token-demo/sa-token-demo-bom-import/pom.xml b/sa-token-demo/sa-token-demo-bom-import/pom.xml index c2dc16f4f87bfc2482947e1fdf2869394e6e00d9..2a03843b0d37298aa1f89eee48aabc2b277dd3a0 100644 --- a/sa-token-demo/sa-token-demo-bom-import/pom.xml +++ b/sa-token-demo/sa-token-demo-bom-import/pom.xml @@ -16,7 +16,7 @@ - 1.37.0 + 1.39.0 @@ -73,7 +73,7 @@ cn.dev33 sa-token-bom - 1.37.0 + 1.39.0 pom import diff --git a/sa-token-demo/sa-token-demo-case/pom.xml b/sa-token-demo/sa-token-demo-case/pom.xml index fb56e1f76f323b95fd9c27f5693608faeeb8f67a..f3bf964cd5b6779fd0d89e4d6d32c80531b080a1 100644 --- a/sa-token-demo/sa-token-demo-case/pom.xml +++ b/sa-token-demo/sa-token-demo-case/pom.xml @@ -17,7 +17,7 @@ - 1.37.0 + 1.39.0 @@ -51,7 +51,7 @@ org.apache.commons commons-pool2
- + org.springframework.boot diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/SaTokenCaseApplication.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/SaTokenCaseApplication.java index 025814fc0c7fb18d3a05a4d67ec1586f69d1b4a9..9f7d505005599d4e51f3bc96e1254adf02fa2d9a 100644 --- a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/SaTokenCaseApplication.java +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/SaTokenCaseApplication.java @@ -1,10 +1,9 @@ package com.pj; +import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import cn.dev33.satoken.SaManager; - /** * Sa-Token 示例 * @author click33 diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/test/TestController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/test/TestController.java index 16204f3ad2b963a85613f790a85bff3f230f4c57..da5aa8eb17574fbaef925e86229a7092d47c8cb6 100644 --- a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/test/TestController.java +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/test/TestController.java @@ -1,10 +1,9 @@ package com.pj.cases.test; +import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import cn.dev33.satoken.util.SaResult; - /** * 测试专用 Controller * @author click33 @@ -17,14 +16,14 @@ public class TestController { // 测试 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") public SaResult test() { - System.out.println("------------进来了"); + System.out.println("------------进来了"); return SaResult.ok(); } // 测试 浏览器访问: http://localhost:8081/test/test2 @RequestMapping("test2") public SaResult test2() { - System.out.println("------------进来了"); + System.out.println("------------进来了"); return SaResult.ok(); } diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SecureController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SecureController.java index 96762b7ce72179fc978f42cb469d1cf0679b0874..a4ec5761d89ca27ece70fdcdd698971d2bd213f8 100644 --- a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SecureController.java +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SecureController.java @@ -51,25 +51,25 @@ public class SecureController { } // RSA加密 ---- http://localhost:8081/secure/rsa - @RequestMapping("rsa") - public SaResult rsa() { - // 定义私钥和公钥 - String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAO+wmt01pwm9lHMdq7A8gkEigk0XKMfjv+4IjAFhWCSiTeP7dtlnceFJbkWxvbc7Qo3fCOpwmfcskwUc3VSgyiJkNJDs9ivPbvlt8IU2bZ+PBDxYxSCJFrgouVOpAr8ar/b6gNuYTi1vt3FkGtSjACFb002/68RKUTye8/tdcVilAgMBAAECgYA1COmrSqTUJeuD8Su9ChZ0HROhxR8T45PjMmbwIz7ilDsR1+E7R4VOKPZKW4Kz2VvnklMhtJqMs4MwXWunvxAaUFzQTTg2Fu/WU8Y9ha14OaWZABfChMZlpkmpJW9arKmI22ZuxCEsFGxghTiJQ3tK8npj5IZq5vk+6mFHQ6aJAQJBAPghz91Dpuj+0bOUfOUmzi22obWCBncAD/0CqCLnJlpfOoa9bOcXSusGuSPuKy5KiGyblHMgKI6bq7gcM2DWrGUCQQD3SkOcmia2s/6i7DUEzMKaB0bkkX4Ela/xrfV+A3GzTPv9bIBamu0VIHznuiZbeNeyw7sVo4/GTItq/zn2QJdBAkEA8xHsVoyXTVeShaDIWJKTFyT5dJ1TR++/udKIcuiNIap34tZdgGPI+EM1yoTduBM7YWlnGwA9urW0mj7F9e9WIQJAFjxqSfmeg40512KP/ed/lCQVXtYqU7U2BfBTg8pBfhLtEcOg4wTNTroGITwe2NjL5HovJ2n2sqkNXEio6Ji0QQJAFLW1Kt80qypMqot+mHhS+0KfdOpaKeMWMSR4Ij5VfE63WzETEeWAMQESxzhavN1WOTb3/p6icgcVbgPQBaWhGg=="; - String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvsJrdNacJvZRzHauwPIJBIoJNFyjH47/uCIwBYVgkok3j+3bZZ3HhSW5Fsb23O0KN3wjqcJn3LJMFHN1UoMoiZDSQ7PYrz275bfCFNm2fjwQ8WMUgiRa4KLlTqQK/Gq/2+oDbmE4tb7dxZBrUowAhW9NNv+vESlE8nvP7XXFYpQIDAQAB"; - - // 文本 - String text = "Sa-Token 一个轻量级java权限认证框架"; - - // 使用公钥加密 - String ciphertext = SaSecureUtil.rsaEncryptByPublic(publicKey, text); - System.out.println("公钥加密后:" + ciphertext); - - // 使用私钥解密 - String text2 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext); - System.out.println("私钥解密后:" + text2); - - return SaResult.ok(); - } +// @RequestMapping("rsa") +// public SaResult rsa() { +// // 定义私钥和公钥 +// String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAO+wmt01pwm9lHMdq7A8gkEigk0XKMfjv+4IjAFhWCSiTeP7dtlnceFJbkWxvbc7Qo3fCOpwmfcskwUc3VSgyiJkNJDs9ivPbvlt8IU2bZ+PBDxYxSCJFrgouVOpAr8ar/b6gNuYTi1vt3FkGtSjACFb002/68RKUTye8/tdcVilAgMBAAECgYA1COmrSqTUJeuD8Su9ChZ0HROhxR8T45PjMmbwIz7ilDsR1+E7R4VOKPZKW4Kz2VvnklMhtJqMs4MwXWunvxAaUFzQTTg2Fu/WU8Y9ha14OaWZABfChMZlpkmpJW9arKmI22ZuxCEsFGxghTiJQ3tK8npj5IZq5vk+6mFHQ6aJAQJBAPghz91Dpuj+0bOUfOUmzi22obWCBncAD/0CqCLnJlpfOoa9bOcXSusGuSPuKy5KiGyblHMgKI6bq7gcM2DWrGUCQQD3SkOcmia2s/6i7DUEzMKaB0bkkX4Ela/xrfV+A3GzTPv9bIBamu0VIHznuiZbeNeyw7sVo4/GTItq/zn2QJdBAkEA8xHsVoyXTVeShaDIWJKTFyT5dJ1TR++/udKIcuiNIap34tZdgGPI+EM1yoTduBM7YWlnGwA9urW0mj7F9e9WIQJAFjxqSfmeg40512KP/ed/lCQVXtYqU7U2BfBTg8pBfhLtEcOg4wTNTroGITwe2NjL5HovJ2n2sqkNXEio6Ji0QQJAFLW1Kt80qypMqot+mHhS+0KfdOpaKeMWMSR4Ij5VfE63WzETEeWAMQESxzhavN1WOTb3/p6icgcVbgPQBaWhGg=="; +// String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvsJrdNacJvZRzHauwPIJBIoJNFyjH47/uCIwBYVgkok3j+3bZZ3HhSW5Fsb23O0KN3wjqcJn3LJMFHN1UoMoiZDSQ7PYrz275bfCFNm2fjwQ8WMUgiRa4KLlTqQK/Gq/2+oDbmE4tb7dxZBrUowAhW9NNv+vESlE8nvP7XXFYpQIDAQAB"; +// +// // 文本 +// String text = "Sa-Token 一个轻量级java权限认证框架"; +// +// // 使用公钥加密 +// String ciphertext = SaSecureUtil.rsaEncryptByPublic(publicKey, text); +// System.out.println("公钥加密后:" + ciphertext); +// +// // 使用私钥解密 +// String text2 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext); +// System.out.println("私钥解密后:" + text2); +// +// return SaResult.ok(); +// } // Base64 编码 ---- http://localhost:8081/secure/base64 @RequestMapping("base64") diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/current/GlobalException.java index fd1f2697801b8638fb03239940e3111e6f42d102..593da152d2fbdd3a590a98aa6f6e78cf909f1766 100644 --- a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/current/GlobalException.java +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/current/GlobalException.java @@ -4,7 +4,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import cn.dev33.satoken.exception.DisableServiceException; -import cn.dev33.satoken.exception.NotBasicAuthException; +import cn.dev33.satoken.exception.NotHttpBasicAuthException; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.exception.NotRoleException; @@ -57,8 +57,8 @@ public class GlobalException { } // 拦截:Http Basic 校验失败异常 - @ExceptionHandler(NotBasicAuthException.class) - public SaResult handlerException(NotBasicAuthException e) { + @ExceptionHandler(NotHttpBasicAuthException.class) + public SaResult handlerException(NotHttpBasicAuthException e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/SaTokenConfigure.java index c5c9f7a5f9a706d6d53b0783b39463d368d5f5e0..76be8691bab2b4a654f974306be523b75a2b039d 100644 --- a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/SaTokenConfigure.java +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -1,19 +1,19 @@ package com.pj.satoken; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.stp.StpUtil; -import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; import cn.dev33.satoken.util.SaResult; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.annotation.PostConstruct; /** @@ -32,14 +32,14 @@ public class SaTokenConfigure implements WebMvcConfigurer { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor(handle -> { // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); - + // 指定一条 match 规则 SaRouter - .match("/user/**") // 拦截的 path 列表,可以写多个 - .notMatch("/user/doLogin", "/user/doLogin2") // 排除掉的 path 列表,可以写多个 - .check(r -> StpUtil.checkLogin()); // 要执行的校验动作,可以写完整的 lambda 表达式 + .match("/user/**") // 拦截的 path 列表,可以写多个 + .notMatch("/user/doLogin", "/user/doLogin2") // 排除掉的 path 列表,可以写多个 + .check(r -> StpUtil.checkLogin()); // 要执行的校验动作,可以写完整的 lambda 表达式 - // 权限校验 -- 不同模块认证不同权限 + // 权限校验 -- 不同模块认证不同权限 SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); @@ -49,16 +49,16 @@ public class SaTokenConfigure implements WebMvcConfigurer { // 甚至你可以随意的写一个打印语句 SaRouter.match("/router/print", r -> System.out.println("----啦啦啦----")); - // 写一个完整的 lambda + // 写一个完整的 lambda SaRouter.match("/router/print2", r -> { System.out.println("----啦啦啦2----"); - // ... 其它代码 + // ... 其它代码 }); - + /* - * 相关路由都定义在 com.pj.cases.use.RouterCheckController 中 + * 相关路由都定义在 com.pj.cases.use.RouterCheckController 中 */ - + })).addPathPatterns("/**"); } @@ -114,11 +114,11 @@ public class SaTokenConfigure implements WebMvcConfigurer { /** * 重写 Sa-Token 框架内部算法策略 */ - @Autowired + @PostConstruct public void rewriteSaStrategy() { // 重写Sa-Token的注解处理器,增加注解合并功能 - SaStrategy.instance.getAnnotation = (element, annotationClass) -> { - return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass); + SaAnnotationStrategy.instance.getAnnotation = (element, annotationClass) -> { + return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass); }; } diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpUserUtil.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpUserUtil.java index ddaf4d25a1a07cb2bebb1aa80e79ed45ec99b3f0..1bbabcf75629539410983e308c1c6afde4c4f051 100644 --- a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpUserUtil.java +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpUserUtil.java @@ -4,18 +4,21 @@ import cn.dev33.satoken.SaManager; import cn.dev33.satoken.fun.SaFunction; import cn.dev33.satoken.listener.SaTokenEventCenter; import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.session.TokenSign; import cn.dev33.satoken.stp.SaLoginModel; import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpLogic; +import org.springframework.stereotype.Component; import java.util.List; /** - * Sa-Token 权限认证工具类 (User 版) + * Sa-Token 权限认证工具类(User版) * * @author click33 * @since 1.0.0 */ +@Component public class StpUserUtil { private StpUserUtil() {} @@ -212,6 +215,16 @@ public class StpUserUtil { return stpLogic.createLoginSession(id, loginModel); } + /** + * 获取指定账号 id 的登录会话数据,如果获取不到则创建并返回 + * + * @param id 账号id,建议的类型:(long | int | String) + * @return 返回会话令牌 + */ + public static String getOrCreateLoginSession(Object id) { + return stpLogic.getOrCreateLoginSession(id); + } + // --- 注销 /** @@ -302,6 +315,15 @@ public class StpUserUtil { return stpLogic.isLogin(); } + /** + * 判断指定账号是否已经登录 + * + * @return 已登录返回 true,未登录返回 false + */ + public static boolean isLogin(Object loginId) { + return stpLogic.isLogin(loginId); + } + /** * 检验当前会话是否已经登录,如未登录,则抛出异常 */ @@ -802,6 +824,17 @@ public class StpUserUtil { return stpLogic.getTokenValueListByLoginId(loginId, device); } + /** + * 获取指定账号 id 指定设备类型端的 tokenSign 集合 + * + * @param loginId 账号id + * @param device 设备类型,填 null 代表不限设备类型 + * @return 此 loginId 的所有登录 tokenSign + */ + public static List getTokenSignListByLoginId(Object loginId, String device) { + return stpLogic.getTokenSignListByLoginId(loginId, device); + } + /** * 返回当前会话的登录设备类型 * @@ -811,6 +844,26 @@ public class StpUserUtil { return stpLogic.getLoginDevice(); } + /** + * 返回指定 token 会话的登录设备类型 + * + * @param tokenValue 指定token + * @return 当前令牌的登录设备类型 + */ + public static String getLoginDeviceByToken(String tokenValue) { + return stpLogic.getLoginDeviceByToken(tokenValue); + } + + /** + * 获取当前 token 的最后活跃时间(13位时间戳),如果不存在则返回 -2 + * + * @return / + */ + public static long getTokenLastActiveTime() { + return stpLogic.getTokenLastActiveTime(); + } + + // ------------------- 会话管理 ------------------- diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/CheckAccount.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/CheckAccount.java new file mode 100644 index 0000000000000000000000000000000000000000..2c01a5ae5a29caac56b1e26bf5dfde3e80e2ce03 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/CheckAccount.java @@ -0,0 +1,33 @@ +package com.pj.satoken.custom_annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 账号校验:在标注一个方法上时,要求前端必须提交相应的账号密码参数才能访问方法。 + * + * @author click33 + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE}) +public @interface CheckAccount { + + /** + * 需要校验的账号 + * + * @return / + */ + String name(); + + /** + * 需要校验的密码 + * + * @return / + */ + String pwd(); + + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckLogin.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckLogin.java new file mode 100644 index 0000000000000000000000000000000000000000..9495168b99c36bd1032fbc7ab2245441c1ce3fb6 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckLogin.java @@ -0,0 +1,18 @@ +package com.pj.satoken.custom_annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 登录认证(User版):只有登录之后才能进入该方法 + *

可标注在函数、类上(效果等同于标注在此类的所有方法上) + * + * @author click33 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE}) +public @interface SaUserCheckLogin { + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckPermission.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckPermission.java new file mode 100644 index 0000000000000000000000000000000000000000..eb980411c938abe0d9488801a88dfb003324cb3e --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckPermission.java @@ -0,0 +1,50 @@ +package com.pj.satoken.custom_annotation; + +import cn.dev33.satoken.annotation.SaMode; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 权限认证(User版):必须具有指定权限才能进入该方法 + *

可标注在函数、类上(效果等同于标注在此类的所有方法上) + * + * @author click33 + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE}) +public @interface SaUserCheckPermission { + + /** + * 需要校验的权限码 + * @return 需要校验的权限码 + */ + String [] value() default {}; + + /** + * 验证模式:AND | OR,默认AND + * @return 验证模式 + */ + SaMode mode() default SaMode.AND; + + /** + * 在权限校验不通过时的次要选择,两者只要其一校验成功即可通过校验 + * + *

+ * 例1:@SaCheckPermission(value="user-add", orRole="admin"), + * 代表本次请求只要具有 user-add权限 或 admin角色 其一即可通过校验。 + *

+ * + *

+ * 例2: orRole = {"admin", "manager", "staff"},具有三个角色其一即可。
+ * 例3: orRole = {"admin, manager, staff"},必须三个角色同时具备。 + *

+ * + * @return / + */ + String[] orRole() default {}; + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckRole.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckRole.java new file mode 100644 index 0000000000000000000000000000000000000000..c3e06bac4a9d3fc477ee8cac39adabd882be5686 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckRole.java @@ -0,0 +1,32 @@ +package com.pj.satoken.custom_annotation; + +import cn.dev33.satoken.annotation.SaMode; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 角色认证(User版):必须具有指定角色标识才能进入该方法 + *

可标注在函数、类上(效果等同于标注在此类的所有方法上) + * @author click33 + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE}) +public @interface SaUserCheckRole { + + /** + * 需要校验的角色标识 + * @return 需要校验的角色标识 + */ + String [] value() default {}; + + /** + * 验证模式:AND | OR,默认AND + * @return 验证模式 + */ + SaMode mode() default SaMode.AND; + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckSafe.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckSafe.java new file mode 100644 index 0000000000000000000000000000000000000000..49e2da67e3999a779f0bdbfe1df10084c7446119 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckSafe.java @@ -0,0 +1,28 @@ +package com.pj.satoken.custom_annotation; + +import cn.dev33.satoken.util.SaTokenConsts; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 二级认证校验(User版):客户端必须完成二级认证之后,才能进入该方法,否则将被抛出异常。 + * + *

可标注在方法、类上(效果等同于标注在此类的所有方法上)。 + * + * @author click33 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface SaUserCheckSafe { + + /** + * 要校验的服务 + * + * @return / + */ + String value() default SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE; + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/CheckAccountHandler.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/CheckAccountHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..2b49badff844e99dd9be577d52f756e8d5d86323 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/CheckAccountHandler.java @@ -0,0 +1,42 @@ +package com.pj.satoken.custom_annotation.handler; + +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.exception.SaTokenException; +import com.pj.satoken.custom_annotation.CheckAccount; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +/** + * 注解 CheckAccount 的处理器 + * + * @author click33 + * + */ +@Component +public class CheckAccountHandler implements SaAnnotationHandlerInterface { + + // 指定这个处理器要处理哪个注解 + @Override + public Class getHandlerAnnotationClass() { + return CheckAccount.class; + } + + // 每次请求校验注解时,会执行的方法 + @Override + public void checkMethod(CheckAccount at, Method method) { + // 获取前端请求提交的参数 + String name = SaHolder.getRequest().getParamNotNull("name"); + String pwd = SaHolder.getRequest().getParamNotNull("pwd"); + + // 与注解中指定的值相比较 + if(name.equals(at.name()) && pwd.equals(at.pwd()) ) { + // 校验通过,什么也不做 + } else { + // 校验不通过,则抛出异常 + throw new SaTokenException("账号或密码错误,未通过校验"); + } + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckLoginHandler.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckLoginHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..73c51865798d3acee09da8dd4a5649a2e01245ed --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckLoginHandler.java @@ -0,0 +1,29 @@ +package com.pj.satoken.custom_annotation.handler; + +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.annotation.handler.SaCheckLoginHandler; +import com.pj.satoken.StpUserUtil; +import com.pj.satoken.custom_annotation.SaUserCheckLogin; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +/** + * 注解 SaUserCheckLogin 的处理器 + * + * @author click33 + */ +@Component +public class SaUserCheckLoginHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaUserCheckLogin.class; + } + + @Override + public void checkMethod(SaUserCheckLogin at, Method method) { + SaCheckLoginHandler._checkMethod(StpUserUtil.TYPE); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckPermissionHandler.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..35ef3a44d66188189fe32128e53db86ab8ef5aef --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckPermissionHandler.java @@ -0,0 +1,29 @@ +package com.pj.satoken.custom_annotation.handler; + +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.annotation.handler.SaCheckPermissionHandler; +import com.pj.satoken.StpUserUtil; +import com.pj.satoken.custom_annotation.SaUserCheckPermission; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +/** + * 注解 SaUserCheckPermission 的处理器 + * + * @author click33 + */ +@Component +public class SaUserCheckPermissionHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaUserCheckPermission.class; + } + + @Override + public void checkMethod(SaUserCheckPermission at, Method method) { + SaCheckPermissionHandler._checkMethod(StpUserUtil.TYPE, at.value(), at.mode(), at.orRole()); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckRoleHandler.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckRoleHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..c430fb1e031ce8ef2f5fadbb5b681c1b9e17554a --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckRoleHandler.java @@ -0,0 +1,29 @@ +package com.pj.satoken.custom_annotation.handler; + +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.annotation.handler.SaCheckRoleHandler; +import com.pj.satoken.StpUserUtil; +import com.pj.satoken.custom_annotation.SaUserCheckRole; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +/** + * 注解 SaUserCheckRole 的处理器 + * + * @author click33 + */ +@Component +public class SaUserCheckRoleHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaUserCheckRole.class; + } + + @Override + public void checkMethod(SaUserCheckRole at, Method method) { + SaCheckRoleHandler._checkMethod(StpUserUtil.TYPE, at.value(), at.mode()); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckSafeHandler.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckSafeHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..88504e06b15f070f25bb772931541ca32067f0cf --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckSafeHandler.java @@ -0,0 +1,29 @@ +package com.pj.satoken.custom_annotation.handler; + +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.annotation.handler.SaCheckSafeHandler; +import com.pj.satoken.StpUserUtil; +import com.pj.satoken.custom_annotation.SaUserCheckSafe; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +/** + * 注解 SaUserCheckPermission 的处理器 + * + * @author click33 + */ +@Component +public class SaUserCheckSafeHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaUserCheckSafe.class; + } + + @Override + public void checkMethod(SaUserCheckSafe at, Method method) { + SaCheckSafeHandler._checkMethod(StpUserUtil.TYPE, at.value()); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/at/SaUserCheckLogin.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckLogin.java similarity index 93% rename from sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/at/SaUserCheckLogin.java rename to sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckLogin.java index 5c6236678c8d504ca51e4bd1148b04c86eda1569..45e60c5f77a09e52cd471d9e912e2de3295740e3 100644 --- a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/at/SaUserCheckLogin.java +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckLogin.java @@ -1,13 +1,13 @@ -package com.pj.satoken.at; +package com.pj.satoken.merge_annotation; + +import cn.dev33.satoken.annotation.SaCheckLogin; +import com.pj.satoken.StpUserUtil; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import cn.dev33.satoken.annotation.SaCheckLogin; -import com.pj.satoken.StpUserUtil; - /** * 登录认证(User版):只有登录之后才能进入该方法 *

可标注在函数、类上(效果等同于标注在此类的所有方法上) diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/at/SaUserCheckPermission.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckPermission.java similarity index 62% rename from sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/at/SaUserCheckPermission.java rename to sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckPermission.java index 22938622f170aabf4faeaa5446664889186d2f63..641f23c6e2178e10d0e88397a7730a7f7ecbb929 100644 --- a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/at/SaUserCheckPermission.java +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckPermission.java @@ -1,16 +1,15 @@ -package com.pj.satoken.at; +package com.pj.satoken.merge_annotation; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaMode; +import com.pj.satoken.StpUserUtil; +import org.springframework.core.annotation.AliasFor; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import com.pj.satoken.StpUserUtil; -import org.springframework.core.annotation.AliasFor; - -import cn.dev33.satoken.annotation.SaCheckPermission; -import cn.dev33.satoken.annotation.SaMode; - /** * 权限认证(User版):必须具有指定权限才能进入该方法 *

可标注在函数、类上(效果等同于标注在此类的所有方法上) @@ -35,5 +34,23 @@ public @interface SaUserCheckPermission { */ @AliasFor(annotation = SaCheckPermission.class) SaMode mode() default SaMode.AND; - + + /** + * 在权限校验不通过时的次要选择,两者只要其一校验成功即可通过校验 + * + *

+ * 例1:@SaCheckPermission(value="user-add", orRole="admin"), + * 代表本次请求只要具有 user-add权限 或 admin角色 其一即可通过校验。 + *

+ * + *

+ * 例2: orRole = {"admin", "manager", "staff"},具有三个角色其一即可。
+ * 例3: orRole = {"admin, manager, staff"},必须三个角色同时具备。 + *

+ * + * @return / + */ + @AliasFor(annotation = SaCheckPermission.class) + String[] orRole() default {}; + } diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/at/SaUserCheckRole.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckRole.java similarity index 96% rename from sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/at/SaUserCheckRole.java rename to sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckRole.java index 66e1f6d091ea7f63785e236b323f10ee1048bab4..ff81b676401138d3d704ab4d6d0d1f120294b7e7 100644 --- a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/at/SaUserCheckRole.java +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckRole.java @@ -1,16 +1,15 @@ -package com.pj.satoken.at; +package com.pj.satoken.merge_annotation; + +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.annotation.SaMode; +import com.pj.satoken.StpUserUtil; +import org.springframework.core.annotation.AliasFor; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import com.pj.satoken.StpUserUtil; -import org.springframework.core.annotation.AliasFor; - -import cn.dev33.satoken.annotation.SaCheckRole; -import cn.dev33.satoken.annotation.SaMode; - /** * 角色认证(User版):必须具有指定角色标识才能进入该方法 *

可标注在函数、类上(效果等同于标注在此类的所有方法上) diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckSafe.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckSafe.java new file mode 100644 index 0000000000000000000000000000000000000000..60bd4e56ea768856a29de57beff813db12fd3560 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckSafe.java @@ -0,0 +1,31 @@ +package com.pj.satoken.merge_annotation; + +import cn.dev33.satoken.annotation.SaCheckSafe; +import cn.dev33.satoken.util.SaTokenConsts; +import com.pj.satoken.StpUserUtil; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 二级认证校验(User版):客户端必须完成二级认证之后,才能进入该方法,否则将被抛出异常。 + * + *

可标注在方法、类上(效果等同于标注在此类的所有方法上)。 + * + * @author click33 + */ +@SaCheckSafe(type = StpUserUtil.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface SaUserCheckSafe { + + /** + * 要校验的服务 + * + * @return / + */ + String value() default SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE; + +} diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/pom.xml b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/pom.xml index fcfdc51de9b34a56dba92b367aa09ec03e05d3df..5954ce96123a16dcdc189935674b2b9d3af41465 100644 --- a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/pom.xml +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/pom.xml @@ -17,7 +17,7 @@ 1.8 3.1.1 - 1.37.0 + 1.39.0 2.7.21 1.4.2 diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/pom.xml b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/pom.xml index 2b4ff53a777cd1a2b01b3360b46d37af0628101e..522a5ada417fdb2ae81e3b91294ab4dc21a02781 100644 --- a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/pom.xml +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/pom.xml @@ -17,7 +17,7 @@ 1.8 3.1.1 - 1.37.0 + 1.39.0 2.7.21 1.4.2 diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/pom.xml b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/pom.xml index 9f55d960fb2a920075c32a2d075a1ad0993712d7..b88b05cfa4980588773e4c94ea253fb26197ccdd 100644 --- a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/pom.xml +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/pom.xml @@ -17,7 +17,7 @@ 1.8 3.1.1 - 1.37.0 + 1.39.0 3.2.2 2.2.2 diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/pom.xml b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/pom.xml index dee3179fba6b2a6a119268bdc919f59b3bdb3f24..cca0260405f7815397fe8c83b2a1abbfb9798049 100644 --- a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/pom.xml +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/pom.xml @@ -17,7 +17,7 @@ 1.8 3.1.1 - 1.37.0 + 1.39.0 3.2.2 2.2.2 diff --git a/sa-token-demo/sa-token-demo-grpc/pom.xml b/sa-token-demo/sa-token-demo-grpc/pom.xml index 5ef2e36cc6548adde220a383f19b11458d61e098..1d0c04f25e2475b16342739e3f5c14faf12bf745 100644 --- a/sa-token-demo/sa-token-demo-grpc/pom.xml +++ b/sa-token-demo/sa-token-demo-grpc/pom.xml @@ -27,7 +27,7 @@ UTF-8 UTF-8 1.18.10 - 1.37.0 + 1.39.0 diff --git a/sa-token-demo/sa-token-demo-hutool-timed-cache/pom.xml b/sa-token-demo/sa-token-demo-hutool-timed-cache/pom.xml index 10250bae5b5dba48fe87c436298cad11dba822d9..3c906b6153930d7c23e1ef602604205c5241f98e 100644 --- a/sa-token-demo/sa-token-demo-hutool-timed-cache/pom.xml +++ b/sa-token-demo/sa-token-demo-hutool-timed-cache/pom.xml @@ -17,7 +17,7 @@ - 1.37.0 + 1.39.0 diff --git a/sa-token-demo/sa-token-demo-jwt/pom.xml b/sa-token-demo/sa-token-demo-jwt/pom.xml index bbe04ad1cf9e9d6ed8cf8f29e8d951d4cf6deacb..d498d6721bd0d8294ba9de2143bafd087a49c35c 100644 --- a/sa-token-demo/sa-token-demo-jwt/pom.xml +++ b/sa-token-demo/sa-token-demo-jwt/pom.xml @@ -16,7 +16,7 @@ - 1.37.0 + 1.39.0 diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/pom.xml b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/pom.xml index d4d0e7f6fde55e2eac2eb2e8bb0dcd5a60c3580a..c32a50023d083975ca263c75931bd48ec54cb701 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/pom.xml +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/pom.xml @@ -17,7 +17,7 @@ 1.8 3.1.1 - 1.37.0 + 1.39.0 diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/oauth2/SaOAuthClientController.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/oauth2/SaOAuthClientController.java index 284901f0f04d0aee555605bf835c6eecfdc63d23..6769068ebd8707d5ad7a57c2a1c2cf92d4486a80 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/oauth2/SaOAuthClientController.java +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/oauth2/SaOAuthClientController.java @@ -1,17 +1,17 @@ package com.pj.oauth2; -import javax.servlet.http.HttpServletRequest; - +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import com.ejlchina.okhttps.OkHttps; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.pj.utils.SoMap; 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; +import javax.servlet.http.HttpServletRequest; /** * Sa-OAuth2 Client端 控制器 @@ -21,10 +21,10 @@ import cn.dev33.satoken.util.SaResult; 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"; // 服务端接口 - + private final String clientId = "1001"; // 应用id + private final String clientSecret = "aaaa-bbbb-cccc-dddd-eeee"; // 应用秘钥 + private final String serverUrl = "http://sa-oauth-server.com:8000"; // 服务端接口 + // 进入首页 @RequestMapping("/") public Object index(HttpServletRequest request) { @@ -34,7 +34,7 @@ public class SaOAuthClientController { // 根据Code码进行登录,获取 Access-Token 和 openid @RequestMapping("/codeLogin") - public SaResult codeLogin(String code) { + public SaResult codeLogin(String code) throws JsonProcessingException { // 调用Server端接口,获取 Access-Token 以及其他信息 String str = OkHttps.sync(serverUrl + "/oauth2/token") .addBodyPara("grant_type", "authorization_code") @@ -45,26 +45,25 @@ public class SaOAuthClientController { .getBody() .toString(); SoMap so = SoMap.getSoMap().setJsonString(str); - System.out.println("返回结果: " + so); + System.out.println("返回结果: " + new ObjectMapper().writeValueAsString(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); + // 根据openid获取其对应的userId + long uid = getUserIdByOpenid(so.getString("openid")); + so.set("uid", uid); // 返回相关参数 StpUtil.login(uid); - return SaResult.data(data); + return SaResult.data(so); } // 根据 Refresh-Token 去刷新 Access-Token @RequestMapping("/refresh") - public SaResult refresh(String refreshToken) { + public SaResult refresh(String refreshToken) throws JsonProcessingException { // 调用Server端接口,通过 Refresh-Token 刷新出一个新的 Access-Token String str = OkHttps.sync(serverUrl + "/oauth2/refresh") .addBodyPara("grant_type", "refresh_token") @@ -75,21 +74,20 @@ public class SaOAuthClientController { .getBody() .toString(); SoMap so = SoMap.getSoMap().setJsonString(str); - System.out.println("返回结果: " + so); + System.out.println("返回结果: " + new ObjectMapper().writeValueAsString(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); + // 返回相关参数 + return SaResult.data(so); } // 模式三:密码式-授权登录 @RequestMapping("/passwordLogin") - public SaResult passwordLogin(String username, String password) { + public SaResult passwordLogin(String username, String password) throws JsonProcessingException { // 模式三:密码式-授权登录 String str = OkHttps.sync(serverUrl + "/oauth2/token") .addBodyPara("grant_type", "password") @@ -101,26 +99,25 @@ public class SaOAuthClientController { .getBody() .toString(); SoMap so = SoMap.getSoMap().setJsonString(str); - System.out.println("返回结果: " + so); + System.out.println("返回结果: " + new ObjectMapper().writeValueAsString(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); + // 根据openid获取其对应的userId + long uid = getUserIdByOpenid(so.getString("openid")); + so.set("uid", uid); // 返回相关参数 StpUtil.login(uid); - return SaResult.data(data); + return SaResult.data(so); } // 模式四:获取应用的 Client-Token @RequestMapping("/clientToken") - public SaResult clientToken() { + public SaResult clientToken() throws JsonProcessingException { // 调用Server端接口 String str = OkHttps.sync(serverUrl + "/oauth2/client_token") .addBodyPara("grant_type", "client_credentials") @@ -130,16 +127,15 @@ public class SaOAuthClientController { .getBody() .toString(); SoMap so = SoMap.getSoMap().setJsonString(str); - System.out.println("返回结果: " + so); + System.out.println("返回结果: " + new ObjectMapper().writeValueAsString(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); + // 返回相关参数 + return SaResult.data(so); } // 注销登录 @@ -151,7 +147,7 @@ public class SaOAuthClientController { // 根据 Access-Token 置换相关的资源: 获取账号昵称、头像、性别等信息 @RequestMapping("/getUserinfo") - public SaResult getUserinfo(String accessToken) { + public SaResult getUserinfo(String accessToken) throws JsonProcessingException { // 调用Server端接口,查询开放的资源 String str = OkHttps.sync(serverUrl + "/oauth2/userinfo") .addBodyPara("access_token", accessToken) @@ -159,16 +155,15 @@ public class SaOAuthClientController { .getBody() .toString(); SoMap so = SoMap.getSoMap().setJsonString(str); - System.out.println("返回结果: " + so); + System.out.println("返回结果: " + new ObjectMapper().writeValueAsString(so)); // code不等于200 代表请求失败 if(so.getInt("code") != 200) { return SaResult.error(so.getString("msg")); } - // 返回相关参数 (data=获取到的资源 ) - SoMap data = so.getMap("data"); - return SaResult.data(data); + // 返回相关参数 (data=获取到的资源 ) + return SaResult.data(so); } // 全局异常拦截 diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/templates/index.html b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/templates/index.html index 9bcfde5367729bf8033628465cd680d42fe2a4ce..6399985ef7c0d798c5d93166d259b6dbc87092c4 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/templates/index.html +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/templates/index.html @@ -42,33 +42,33 @@

模式一:授权码(Authorization Code)

授权码:OAuth2.0标准授权流程,先 (重定向) 获取Code授权码,再 (Rest API) 获取 Access-Token 和 Openid

- + - 当请求链接不包含scope权限时,将无需用户手动确认,做到静默授权,当然此时我们也只能获取openid - http://sa-oauth-server.com:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/ + 当请求链接不包含 scope 权限,或请求的 scope 近期已授权时,将无需用户手动确认,做到静默授权 + http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/ - + - 当请求链接包含具体的scope权限时,将需要用户手动确认,此时我们除了openid以外还可以获取更多的资源 - http://sa-oauth-server.com:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo + 当请求链接包含具体的 scope 权限时,将需要用户手动确认,此时 OAuth-Server 会返回更多的数据 + http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=openid,userinfo 我们可以拿着 Refresh-Token 去刷新我们的 Access-Token,每次刷新后旧Token将作废 - http://sa-oauth-server.com:8001/oauth2/refresh?grant_type=refresh_token&client_id={value}&client_secret={value}&refresh_token={value} + http://sa-oauth-server.com:8000/oauth2/refresh?grant_type=refresh_token&client_id={value}&client_secret={value}&refresh_token={value} 使用 Access-Token 置换资源: 获取账号昵称、头像、性别等信息 (Access-Token具备userinfo权限时才可以获取成功) - http://sa-oauth-server.com:8001/oauth2/userinfo?access_token={value} + http://sa-oauth-server.com:8000/oauth2/userinfo?access_token={value}

模式二:隐藏式(Implicit)

- + 越过授权码的步骤,直接返回token到前端页面( 格式:http//:domain.com#token=xxxx-xxxx ) - http://sa-oauth-server.com:8001/oauth2/authorize?response_type=token&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo + http://sa-oauth-server.com:8000/oauth2/authorize?response_type=token&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo

模式三:密码式(Password)

@@ -76,18 +76,18 @@ 账号: 密码: - http://sa-oauth-server.com:8001/oauth2/token?grant_type=password&client_id={value}&client_secret={value}&username={value}&password={value} + http://sa-oauth-server.com:8000/oauth2/token?grant_type=password&client_id={value}&client_secret={value}&username={value}&password={value}

模式四:凭证式(Client Credentials)

以上三种模式获取的都是用户的 Access-Token,代表用户对第三方应用的授权,在OAuth2.0中还有一种针对 Client级别的授权, 即:Client-Token,代表应用自身的资源授权

-

Client-Token具有延迟作废特性,即:在每次获取最新Client-Token的时候,旧Client-Token不会立即过期,而是作为Past-Token再次 +

Client-Token具有延迟作废特性,即:在每次获取最新Client-Token的时候,旧Client-Token不会立即过期,而是作为Lower-Client-Token再次 储存起来,资源请求方只要携带其中之一便可通过Token校验,这种特性保证了在大量并发请求时不会出现“新旧Token交替造成的授权失效”, 保证了服务的高可用

- http://sa-oauth-server.com:8001/oauth2/client_token?grant_type=client_credentials&client_id={value}&client_secret={value} + http://sa-oauth-server.com:8000/oauth2/client_token?grant_type=client_credentials&client_id={value}&client_secret={value}

更多资料请参考 Sa-Token 官方文档地址: diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/pom.xml b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/pom.xml index c401619352bc747e435d3efb3ac74799962567f1..2eb121b303f49138b9cdbc5c17f5b391dd40c112 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/pom.xml +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/pom.xml @@ -17,7 +17,7 @@ 1.8 3.1.1 - 1.37.0 + 1.39.0
@@ -58,13 +58,20 @@ org.springframework.boot spring-boot-starter-thymeleaf - - + + + cn.dev33 + sa-token-jwt + ${sa-token.version} + + + + diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/SaOAuth2ServerApplication.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/SaOAuth2ServerApplication.java index 4accb81c8e30b931daeb2d4f2259878dc5c0ceaa..e7bef93581e717df0f0f46fbd809e6e4d18d661c 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/SaOAuth2ServerApplication.java +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/SaOAuth2ServerApplication.java @@ -1,18 +1,20 @@ package com.pj; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** - * 启动:Sa-OAuth2 Server端 - * @author click33 + * 启动:Sa-OAuth2 Server端 + * @author click33 */ -@SpringBootApplication +@SpringBootApplication public class SaOAuth2ServerApplication { public static void main(String[] args) { SpringApplication.run(SaOAuth2ServerApplication.class, args); - System.out.println("\nSa-Token-OAuth Server端启动成功"); + System.out.println("\nSa-Token-OAuth2 Server端启动成功,配置如下:"); + System.out.println(SaOAuth2Manager.getServerConfig()); } } diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2DataLoaderImpl.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2DataLoaderImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..2f582696d424a64d85d378a383361b85bf444eed --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2DataLoaderImpl.java @@ -0,0 +1,46 @@ +package com.pj.oauth2; + +import cn.dev33.satoken.oauth2.consts.GrantType; +import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader; +import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; +import org.springframework.stereotype.Component; + +/** + * Sa-Token OAuth2:自定义数据加载器 + * + * @author click33 + */ +@Component +public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { + + // 根据 clientId 获取 Client 信息 + @Override + public SaClientModel getClientModel(String clientId) { + // 此为模拟数据,真实环境需要从数据库查询 + if("1001".equals(clientId)) { + return new SaClientModel() + .setClientId("1001") // client id + .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥 + .addAllowRedirectUris("*") // 所有允许授权的 url + .addContractScopes("openid", "userid", "userinfo", "oidc") // 所有签约的权限 + .addAllowGrantTypes( // 所有允许的授权模式 + GrantType.authorization_code, // 授权码式 + GrantType.implicit, // 隐式式 + GrantType.refresh_token, // 刷新令牌 + GrantType.password, // 密码式 + GrantType.client_credentials, // 客户端模式 + "phone_code" // 自定义授权模式 手机号验证码登录 + ) + ; + } + return null; + } + + // 根据 clientId 和 loginId 获取 openid + @Override + public String getOpenid(String clientId, Object loginId) { + // 此处使用框架默认算法生成 openid,真实环境建议改为从数据库查询 + return SaOAuth2DataLoader.super.getOpenid(clientId, loginId); + } + +} diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java index 7f49f7466d08e8edb015a556b6db117e0d33bdc8..fee73b0641c7f7ed710b5c0c52926c4eccbe9c02 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java @@ -1,92 +1,86 @@ package com.pj.oauth2; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; +import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor; +import cn.dev33.satoken.oauth2.template.SaOAuth2Util; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; 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; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; /** - * Sa-OAuth2 Server端 控制器 + * Sa-Token-OAuth2 Server端 Controller + * * @author click33 - * */ @RestController public class SaOAuth2ServerController { - // 处理所有OAuth相关请求 + // OAuth2-Server 端:处理所有 OAuth2 相关请求 @RequestMapping("/oauth2/*") public Object request() { System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl()); - return SaOAuth2Handle.serverRequest(); + return SaOAuth2ServerProcessor.instance.dister(); } - - // Sa-OAuth2 定制化配置 + + // Sa-Token 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 map = new HashMap<>(); - map.put("clientId", clientId); - map.put("scope", scope); - return new ModelAndView("confirm.html", map); - }) - ; - } + public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { + // 未登录的视图 + oauth2Server.notLoginView = ()->{ + return new ModelAndView("login.html"); + }; + + // 登录处理函数 + oauth2Server.doLoginHandle = (name, pwd) -> { + if("sa".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok(); + } + return SaResult.error("账号名或密码错误"); + }; + + // 授权确认视图 + oauth2Server.confirmView = (clientId, scopes)->{ + Map map = new HashMap<>(); + map.put("clientId", clientId); + map.put("scope", scopes); + return new ModelAndView("confirm.html", map); + }; - // 全局异常拦截 - @ExceptionHandler - public SaResult handlerException(Exception e) { - e.printStackTrace(); - return SaResult.error(e.getMessage()); } - - + + // ---------- 开放相关资源接口: Client端根据 Access-Token ,置换相关资源 ------------ - // 获取Userinfo信息:昵称、头像、性别等等 + // 获取 userinfo 信息:昵称、头像、性别等等 @RequestMapping("/oauth2/userinfo") public SaResult userinfo() { - // 获取 Access-Token 对应的账号id - String accessToken = SaHolder.getRequest().getParamNotNull("access_token"); + // 获取 Access-Token 对应的账号id + String accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest()); Object loginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken); System.out.println("-------- 此Access-Token对应的账号id: " + loginId); - + // 校验 Access-Token 是否具有权限: userinfo - SaOAuth2Util.checkScope(accessToken, "userinfo"); - + SaOAuth2Util.checkAccessTokenScope(accessToken, "userinfo"); + // 模拟账号信息 (真实环境需要查询数据库获取信息) - Map map = new LinkedHashMap(); - map.put("nickname", "shengzhang_"); + Map map = new LinkedHashMap<>(); + // map.put("userId", loginId); 一般原则下,oauth2-server 不能把 userId 返回给 oauth2-client + map.put("nickname", "林小林"); map.put("avatar", "http://xxx.com/1.jpg"); map.put("age", "18"); map.put("sex", "男"); map.put("address", "山东省 青岛市 城阳区"); - return SaResult.data(map); + return SaResult.ok().setMap(map); } - + } diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2TemplateImpl.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2TemplateImpl.java deleted file mode 100644 index 758b40905c0b000262b0fcef6b49000119bb9095..0000000000000000000000000000000000000000 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2TemplateImpl.java +++ /dev/null @@ -1,39 +0,0 @@ -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 click33 - */ -@Component -public class SaOAuth2TemplateImpl extends SaOAuth2Template { - - // 根据 id 获取 Client 信息 - @Override - public SaClientModel getClientModel(String clientId) { - // 此为模拟数据,真实环境需要从数据库查询 - if("1001".equals(clientId)) { - return new SaClientModel() - .setClientId("1001") - .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") - .setAllowUrl("*") - .setContractScope("userinfo") - .setIsAutoMode(true); - } - return null; - } - - // 根据ClientId 和 LoginId 获取openid - @Override - public String getOpenid(String clientId, Object loginId) { - // 此为模拟数据,真实环境需要从数据库查询 - return "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__"; - } - - // -------------- 其它需要重写的函数 - -} diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom/CustomOidcScopeHandler.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom/CustomOidcScopeHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..835cc3776be3bf167179f49057316edb7bf64979 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom/CustomOidcScopeHandler.java @@ -0,0 +1,32 @@ +//package com.pj.oauth2.custom; +// +//import cn.dev33.satoken.oauth2.data.model.oidc.IdTokenModel; +//import cn.dev33.satoken.oauth2.scope.handler.OidcScopeHandler; +//import org.springframework.stereotype.Component; +// +///** +// * 扩展 OIDC 权限处理器,返回更多字段 +// * +// * @author click33 +// * @since 2024/8/24 +// */ +//@Component +//public class CustomOidcScopeHandler extends OidcScopeHandler { +// +// @Override +// public IdTokenModel workExtraData(IdTokenModel idToken) { +// Object userId = idToken.sub; +// System.out.println("----- 为 idToken 追加扩展字段 ----- "); +// +// idToken.extraData.put("uid", userId); // 用户id +// idToken.extraData.put("nickname", "lin_xiao_lin"); // 昵称 +// idToken.extraData.put("picture", "https://sa-token.cc/logo.png"); // 头像 +// idToken.extraData.put("email", "456456@xx.com"); // 邮箱 +// idToken.extraData.put("phone_number", "13144556677"); // 手机号 +// // 更多字段 ... +// // 可参考:https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims +// +// return idToken; +// } +// +//} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom/PhoneCodeGrantTypeHandler.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom/PhoneCodeGrantTypeHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..da77857d595871bfff91aa95ac8dc2784b517d2f --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom/PhoneCodeGrantTypeHandler.java @@ -0,0 +1,57 @@ +//package com.pj.oauth2.custom; +// +//import cn.dev33.satoken.SaManager; +//import cn.dev33.satoken.context.model.SaRequest; +//import cn.dev33.satoken.oauth2.SaOAuth2Manager; +//import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +//import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; +//import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; +//import cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface; +//import org.springframework.stereotype.Component; +// +//import java.util.List; +// +///** +// * 自定义 phone_code 授权模式处理器 +// * +// * @author click33 +// * @since 2024/8/23 +// */ +//@Component +//public class PhoneCodeGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface { +// +// @Override +// public String getHandlerGrantType() { +// return "phone_code"; +// } +// +// @Override +// public AccessTokenModel getAccessToken(SaRequest req, String clientId, List scopes) { +// +// // 获取前端提交的参数 +// String phone = req.getParamNotNull("phone"); +// String code = req.getParamNotNull("code"); +// String realCode = SaManager.getSaTokenDao().get("phone_code:" + phone); +// +// // 1、校验验证码是否正确 +// if(!code.equals(realCode)) { +// throw new SaOAuth2Exception("验证码错误"); +// } +// +// // 2、校验通过,删除验证码 +// SaManager.getSaTokenDao().delete("phone_code:" + phone); +// +// // 3、登录 +// long userId = 10001; // 模拟 userId,真实项目应该根据手机号从数据库查询 +// +// // 4、构建 ra 对象 +// RequestAuthModel ra = new RequestAuthModel(); +// ra.clientId = clientId; +// ra.loginId = userId; +// ra.scopes = scopes; +// +// // 5、生成 Access-Token +// AccessTokenModel at = SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true); +// return at; +// } +//} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom/PhoneLoginController.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom/PhoneLoginController.java new file mode 100644 index 0000000000000000000000000000000000000000..8f25a174244ec3ce3c4e42a5fb796fb2a5ea47b4 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom/PhoneLoginController.java @@ -0,0 +1,26 @@ +//package com.pj.oauth2.custom; +// +//import cn.dev33.satoken.SaManager; +//import cn.dev33.satoken.util.SaFoxUtil; +//import cn.dev33.satoken.util.SaResult; +//import org.springframework.web.bind.annotation.RequestMapping; +//import org.springframework.web.bind.annotation.RestController; +// +///** +// * 自定义手机登录接口 +// * +// * @author click33 +// * @since 2024/8/23 +// */ +//@RestController +//public class PhoneLoginController { +// +// @RequestMapping("/oauth2/sendPhoneCode") +// public SaResult sendCode(String phone) { +// String code = SaFoxUtil.getRandomNumber(100000, 999999) + ""; +// SaManager.getSaTokenDao().set("phone_code:" + phone, code, 60 * 5); +// System.out.println("手机号:" + phone + ",验证码:" + code + ",已发送成功"); +// return SaResult.ok("验证码发送成功"); +// } +// +//} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom/UserinfoScopeHandler.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom/UserinfoScopeHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..bc71bebe2f222699f803f55b9d705984dca806cc --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom/UserinfoScopeHandler.java @@ -0,0 +1,41 @@ +//package com.pj.oauth2.custom; +// +//import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +//import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +//import cn.dev33.satoken.oauth2.scope.handler.SaOAuth2ScopeHandlerInterface; +//import org.springframework.stereotype.Component; +// +//import java.util.LinkedHashMap; +//import java.util.Map; +// +///** +// * @author click33 +// * @since 2024/8/20 +// */ +//@Component +//public class UserinfoScopeHandler implements SaOAuth2ScopeHandlerInterface { +// +// @Override +// public String getHandlerScope() { +// return "userinfo"; +// } +// +// @Override +// public void workAccessToken(AccessTokenModel at) { +// System.out.println("--------- userinfo 权限,加工 AccessTokenModel --------- "); +// // 模拟账号信息 (真实环境需要查询数据库获取信息) +// Map map = new LinkedHashMap(); +// map.put("userId", "10008"); +// map.put("nickname", "shengzhang_"); +// map.put("avatar", "http://xxx.com/1.jpg"); +// map.put("age", "18"); +// map.put("sex", "男"); +// map.put("address", "山东省 青岛市 城阳区"); +// at.extraData.put("userinfo", map); +// } +// +// @Override +// public void workClientToken(ClientTokenModel ct) { +// } +// +//} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/satoken/GlobalExceptionHandler.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/satoken/GlobalExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..b97704fa67105ab1dee14fe7f3550aab5efb25e3 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/satoken/GlobalExceptionHandler.java @@ -0,0 +1,22 @@ +package com.pj.satoken; + +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 全局异常处理 + * @author click33 + * + */ +@RestControllerAdvice +public class GlobalExceptionHandler { + + // 全局异常拦截 + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 0000000000000000000000000000000000000000..eadad0f33655fa0f1a8adc4cbf28b2c072a8aa21 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,27 @@ +package com.pj.satoken; + +import cn.dev33.satoken.interceptor.SaInterceptor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + + +/** + * [Sa-Token 权限认证] 配置类 + * @author click33 + * + */ +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + + /** + * 注册 Sa-Token 拦截器打开注解鉴权功能 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 注册 Sa-Token 拦截器打开注解鉴权功能 + registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); + + } + +} diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/test/TestController.java new file mode 100644 index 0000000000000000000000000000000000000000..8adc82dcf4de79e6b1dd0ee9c72be23325c00a6c --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,72 @@ +package com.pj.test; + +import cn.dev33.satoken.oauth2.annotation.SaCheckAccessToken; +import cn.dev33.satoken.oauth2.annotation.SaCheckClientIdSecret; +import cn.dev33.satoken.oauth2.annotation.SaCheckClientToken; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * OAuth2 相关注解测试 Controller + * + * @author click33 + * @since 2024/8/25 + */ +@RestController +@RequestMapping("/test") +public class TestController { + + // 测试:携带有效的 access_token 才可以进入请求 + // 你可以在请求参数中携带 access_token 参数,或者从请求头以 Authorization: bearer xxx 的形式携带 + @SaCheckAccessToken + @RequestMapping("/checkAccessToken") + public SaResult checkAccessToken() { + return SaResult.ok("访问成功"); + } + + // 测试:携带有效的 access_token ,并且具备指定 scope 才可以进入请求 + @SaCheckAccessToken(scope = "userinfo") + @RequestMapping("/checkAccessTokenScope") + public SaResult checkAccessTokenScope() { + return SaResult.ok("访问成功"); + } + + // 测试:携带有效的 access_token ,并且具备指定 scope 列表才可以进入请求 + @SaCheckAccessToken(scope = {"openid", "userinfo"}) + @RequestMapping("/checkAccessTokenScopeList") + public SaResult checkAccessTokenScopeList() { + return SaResult.ok("访问成功"); + } + + // 测试:携带有效的 client_token 才可以进入请求 + // 你可以在请求参数中携带 client_token 参数,或者从请求头以 Authorization: bearer xxx 的形式携带 + @SaCheckClientToken + @RequestMapping("/checkClientToken") + public SaResult checkClientToken() { + return SaResult.ok("访问成功"); + } + + // 测试:携带有效的 client_token ,并且具备指定 scope 才可以进入请求 + @SaCheckClientToken(scope = "userinfo") + @RequestMapping("/checkClientTokenScope") + public SaResult checkClientTokenScope() { + return SaResult.ok("访问成功"); + } + + // 测试:携带有效的 client_token ,并且具备指定 scope 列表才可以进入请求 + @SaCheckClientToken(scope = {"openid", "userinfo"}) + @RequestMapping("/checkClientTokenScopeList") + public SaResult checkClientTokenScopeList() { + return SaResult.ok("访问成功"); + } + + // 测试:携带有效的 client_id 和 client_secret 信息,才可以进入请求 + // 你可以在请求参数中携带 client_id 和 client_secret 参数,或者从请求头以 Authorization: Basic base64(client_id:client_secret) 的形式携带 + @SaCheckClientIdSecret + @RequestMapping("/checkClientIdSecret") + public SaResult checkClientIdSecret() { + return SaResult.ok("访问成功"); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml index f745a8d9bd2eac9186a928c79f274fa5ba9430b3..5b2e9ebb83ad603b8ad96c742a6851e2c61e831d 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml @@ -1,19 +1,31 @@ -server: - port: 8001 +server: + port: 8000 # 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: - # redis配置 +sa-token: + # token名称 (同时也是 Cookie 名称) + token-name: satoken + # 是否打印操作日志 + is-log: true + # jwt 秘钥 + jwt-secret-key: saxsaxsaxsax + # OAuth2.0 配置 + oauth2-server: + # 是否全局开启授权码模式 + enable-authorization-code: true + # 是否全局开启 Implicit 模式 + enable-implicit: true + # 是否全局开启密码模式 + enable-password: true + # 是否全局开启客户端模式 + enable-client-credentials: true + # 定义哪些 scope 是高级权限,多个用逗号隔开 + # higher-scope: openid,userid + # 定义哪些 scope 是低级权限,多个用逗号隔开 + # lower-scope: userinfo + +spring: + # redis配置 redis: # Redis数据库索引(默认为0) database: 1 @@ -22,7 +34,7 @@ spring: # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) - # password: + # password: # 连接超时时间(毫秒) timeout: 1000ms lettuce: @@ -35,6 +47,5 @@ spring: max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 - - - \ No newline at end of file + + diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/templates/confirm.html b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/templates/confirm.html index 7ede1714d42ae9a22b4d54cab65ff4255e71b730..cfeaf0993c0765406e634853948c88df2a4c0712 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/templates/confirm.html +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/templates/confirm.html @@ -33,20 +33,31 @@ console.log('-----------'); $.ajax({ url: '/oauth2/doConfirm', + method: "POST", data: { client_id: getParam('client_id'), - scope: getParam('scope') + scope: getParam('scope'), + // 以下四个参数必须一起出现 + build_redirect_uri: true, + response_type: getParam('response_type'), + redirect_uri: getParam('redirect_uri'), + state: getParam('state'), }, dataType: 'json', success: function(res) { - if(res.code == 200) { + console.log('res:', res); + if(res.code === 200) { layer.msg('授权成功!'); setTimeout(function() { - location.reload(true); + if (res.redirect_uri) { + location.href = res.redirect_uri; + } else { + location.reload(); + } }, 800); } else { // 重定向至授权失败URL - layer.alert('授权失败!'); + layer.alert('授权失败:' + res.msg); } }, error: function(e) { diff --git a/sa-token-demo/sa-token-demo-quick-login/pom.xml b/sa-token-demo/sa-token-demo-quick-login/pom.xml index 8baf99141c902133492fc40ee4b99b331c00dbea..549954f02997efdc35f4e9eef4d4320e33492a05 100644 --- a/sa-token-demo/sa-token-demo-quick-login/pom.xml +++ b/sa-token-demo/sa-token-demo-quick-login/pom.xml @@ -16,7 +16,7 @@ - 1.37.0 + 1.39.0 diff --git a/sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/pom.xml b/sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/pom.xml index 91a781cc608a7bdaafc1c6bed4d9d55a85a808a5..53bdb8517edeca1d72af578a9e34b02e7722a233 100644 --- a/sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/pom.xml +++ b/sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/pom.xml @@ -17,7 +17,7 @@ - 1.37.0 + 1.39.0 diff --git a/sa-token-demo/sa-token-demo-solon-redisson/pom.xml b/sa-token-demo/sa-token-demo-solon-redisson/pom.xml index 5a9fdf68727409ad3a45fd062ad976129bfdd1d4..6118605f312e0529483ddd68d00ecef29cda0b44 100644 --- a/sa-token-demo/sa-token-demo-solon-redisson/pom.xml +++ b/sa-token-demo/sa-token-demo-solon-redisson/pom.xml @@ -10,13 +10,13 @@ org.noear solon-parent - 2.4.0 + 2.7.0 - 1.37.0 + 1.39.0 UTF-8 UTF-8 diff --git a/sa-token-demo/sa-token-demo-solon/pom.xml b/sa-token-demo/sa-token-demo-solon/pom.xml index 4d41d9090da224ec43fafd0e3b638419c3139a1c..6edda2873404157ba8f46846915c7fc8d327c27e 100644 --- a/sa-token-demo/sa-token-demo-solon/pom.xml +++ b/sa-token-demo/sa-token-demo-solon/pom.xml @@ -10,13 +10,16 @@ org.noear solon-parent - 2.4.0 + 2.9.1 - 1.37.0 + 17 + 17 + 17 + 1.39.0 UTF-8 UTF-8 @@ -40,7 +43,7 @@ sa-token-solon-plugin ${sa-token.version} - + cn.dev33 diff --git a/sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/SaTokenConfigure.java index 1ca07d1737b16721b65c07ee284f559ca2c692f8..9951566eda59153258cebbd7b5da6f0b6a1c084b 100644 --- a/sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/SaTokenConfigure.java +++ b/sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -1,15 +1,14 @@ package com.pj.satoken; +import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoOfRedis; +import cn.dev33.satoken.dao.SaTokenDaoOfRedisJson; import cn.dev33.satoken.solon.integration.SaTokenInterceptor; +import com.pj.util.AjaxJson; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Configuration; - -import com.pj.util.AjaxJson; - -import cn.dev33.satoken.context.SaHolder; import org.noear.solon.annotation.Inject; @@ -59,9 +58,11 @@ public class SaTokenConfigure { ; }); } -//如果需要 redis dao,加这段代表 -// @Bean -// public SaTokenDao saTokenDaoInit(@Inject("${sa-token-dao.redis}") SaTokenDaoOfRedis saTokenDao) { -// return saTokenDao; -// } + + //如果需要 redis dao,加这段代表 + @Bean + public SaTokenDao saTokenDaoInit(@Inject("${sa-token-dao.redis}") SaTokenDaoOfRedisJson saTokenDao) { + return saTokenDao; + } + } diff --git a/sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/custom_annotation/CheckAccount.java b/sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/custom_annotation/CheckAccount.java new file mode 100644 index 0000000000000000000000000000000000000000..2c01a5ae5a29caac56b1e26bf5dfde3e80e2ce03 --- /dev/null +++ b/sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/custom_annotation/CheckAccount.java @@ -0,0 +1,33 @@ +package com.pj.satoken.custom_annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 账号校验:在标注一个方法上时,要求前端必须提交相应的账号密码参数才能访问方法。 + * + * @author click33 + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE}) +public @interface CheckAccount { + + /** + * 需要校验的账号 + * + * @return / + */ + String name(); + + /** + * 需要校验的密码 + * + * @return / + */ + String pwd(); + + +} diff --git a/sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/custom_annotation/handler/CheckAccountHandler.java b/sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/custom_annotation/handler/CheckAccountHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..e7c608f20e5f3923aad1b437d5856225416ccf5c --- /dev/null +++ b/sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/custom_annotation/handler/CheckAccountHandler.java @@ -0,0 +1,42 @@ +package com.pj.satoken.custom_annotation.handler; + +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.exception.SaTokenException; +import com.pj.satoken.custom_annotation.CheckAccount; +import org.noear.solon.annotation.Component; + +import java.lang.reflect.Method; + +/** + * 注解 CheckAccount 的处理器 + * + * @author click33 + * + */ +@Component +public class CheckAccountHandler implements SaAnnotationHandlerInterface { + + // 指定这个处理器要处理哪个注解 + @Override + public Class getHandlerAnnotationClass() { + return CheckAccount.class; + } + + // 每次请求校验注解时,会执行的方法 + @Override + public void checkMethod(CheckAccount at, Method method) { + // 获取前端请求提交的参数 + String name = SaHolder.getRequest().getParamNotNull("name"); + String pwd = SaHolder.getRequest().getParamNotNull("pwd"); + + // 与注解中指定的值相比较 + if(name.equals(at.name()) && pwd.equals(at.pwd()) ) { + // 校验通过,什么也不做 + } else { + // 校验不通过,则抛出异常 + throw new SaTokenException("账号或密码错误,未通过校验"); + } + } + +} diff --git a/sa-token-demo/sa-token-demo-solon/src/main/resources/app.yml b/sa-token-demo/sa-token-demo-solon/src/main/resources/app.yml index 630631a71823c83a79c92710b22066de07253791..e3ecff99d4419583bdb18761f4a9b114651f6034 100644 --- a/sa-token-demo/sa-token-demo-solon/src/main/resources/app.yml +++ b/sa-token-demo/sa-token-demo-solon/src/main/resources/app.yml @@ -23,7 +23,7 @@ sa-token: sa-token-dao: #名字可以随意取 redis: server: "localhost:6379" - password: 123456 +# password: 123456 db: 1 maxTotal: 200 diff --git a/sa-token-demo/sa-token-demo-springboot-redis/pom.xml b/sa-token-demo/sa-token-demo-springboot-redis/pom.xml index 8fab3f090536c061619f3d93bcbea9a0fb5f53f0..261b2252e31209be74f3b50373a7ed1f53e2aa9c 100644 --- a/sa-token-demo/sa-token-demo-springboot-redis/pom.xml +++ b/sa-token-demo/sa-token-demo-springboot-redis/pom.xml @@ -17,7 +17,7 @@ - 1.37.0 + 1.39.0 diff --git a/sa-token-demo/sa-token-demo-springboot-redisson/pom.xml b/sa-token-demo/sa-token-demo-springboot-redisson/pom.xml index 6dbb9a1dcc67fddb7992d140c9f0bfe8fbcb3155..351e9fbeecbe61cffc54fc57aa59169f20182588 100644 --- a/sa-token-demo/sa-token-demo-springboot-redisson/pom.xml +++ b/sa-token-demo/sa-token-demo-springboot-redisson/pom.xml @@ -17,7 +17,7 @@ - 1.37.0 + 1.39.0 diff --git a/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/test/AtController.java b/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/test/AtController.java index 3af4c60395d3ef8d2c265e6c82fed506010fbb5b..9235834888d364cfd6fd901cba32cc18275d7f7f 100644 --- a/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/test/AtController.java +++ b/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/test/AtController.java @@ -1,14 +1,9 @@ package com.pj.test; +import cn.dev33.satoken.annotation.*; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -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.annotation.SaMode; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; @@ -71,7 +66,7 @@ public class AtController { } // 通过Basic认证后才可以进入 ---- http://localhost:8081/at/checkBasic - @SaCheckBasic(account = "sa:123456") + @SaCheckHttpBasic(account = "sa:123456") @RequestMapping("checkBasic") public SaResult checkBasic() { return SaResult.ok(); diff --git a/sa-token-demo/sa-token-demo-springboot/pom.xml b/sa-token-demo/sa-token-demo-springboot/pom.xml index d76bfed3d3d53ed77720fab8f0e80d0eaea2b060..3fe2c51b8839483696cc835ea1d1d41665f1231f 100644 --- a/sa-token-demo/sa-token-demo-springboot/pom.xml +++ b/sa-token-demo/sa-token-demo-springboot/pom.xml @@ -17,7 +17,7 @@ - 1.37.0 + 1.39.0 diff --git a/sa-token-demo/sa-token-demo-springboot3-redis/pom.xml b/sa-token-demo/sa-token-demo-springboot3-redis/pom.xml index e745f7dd231732d432cd0922df40facf25d7113c..f717e27262f2760f021b76fd54dd68da334c9d88 100644 --- a/sa-token-demo/sa-token-demo-springboot3-redis/pom.xml +++ b/sa-token-demo/sa-token-demo-springboot3-redis/pom.xml @@ -16,7 +16,7 @@ - 1.37.0 + 1.39.0 diff --git a/sa-token-demo/sa-token-demo-ssm/pom.xml b/sa-token-demo/sa-token-demo-ssm/pom.xml index fea3458dc9ed1144e78a6669d2f61e50bb0343ae..5a23365fd17dc69cc38928fd0a57795fac93dbbb 100644 --- a/sa-token-demo/sa-token-demo-ssm/pom.xml +++ b/sa-token-demo/sa-token-demo-ssm/pom.xml @@ -27,7 +27,7 @@ 5.3.7 2.16.1 - 1.37.0 + 1.39.0 diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/AtController.java b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/AtController.java index 88176cc0113274298b719d986c0c7e14c1dbe380..ee0e9b36b20de15d6ab5e85e39601b425fc47d6d 100644 --- a/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/AtController.java +++ b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/AtController.java @@ -65,7 +65,7 @@ public class AtController { } // 通过Basic认证后才可以进入 ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkBasic - @SaCheckBasic(account = "sa:123456") + @SaCheckHttpBasic(account = "sa:123456") @RequestMapping("checkBasic") public SaResult checkBasic() { return SaResult.ok(); diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/pom.xml b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/pom.xml index dd081fd96701f14aaadc8c142ee9c6e9218e4df3..e0f63a106e3383fba22b28109100597b900155e1 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/pom.xml +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/pom.xml @@ -10,14 +10,14 @@ org.noear solon-parent - 2.4.0 + 2.7.0 - 1.37.0 - 2.2.3 + 1.39.0 + 2.7.0 @@ -49,7 +49,7 @@ sa-token-redisx ${sa-token.version} - + org.noear diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/SaSsoServerApp.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/SaSsoServerApp.java index 82fae5d66f4367d11713341b51dc9f3a3a8ea617..f10580bc710b4492ef1027c48ee08cff1ebdbefd 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/SaSsoServerApp.java +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/SaSsoServerApp.java @@ -1,6 +1,7 @@ package com.pj; +import cn.dev33.satoken.sso.SaSsoManager; import org.noear.solon.Solon; import org.noear.solon.annotation.SolonMain; @@ -9,7 +10,13 @@ public class SaSsoServerApp { public static void main(String[] args) { Solon.start(SaSsoServerApp.class, args); - System.out.println("\n------ Sa-Token-SSO 统一认证中心启动成功 "); + + System.out.println(); + System.out.println("---------------------- Solon Sa-Token SSO 统一认证中心启动成功 ----------------------"); + System.out.println("配置信息:" + SaSsoManager.getServerConfig()); + System.out.println("统一认证登录地址:http://sa-sso-server.com:9000/sso/auth"); + System.out.println("测试前需要根据官网文档修改hosts文件,测试账号密码:sa / 123456"); + System.out.println(); } } \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/SsoConfig.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/SsoConfig.java index a2618dcc6d8037f221eb0ae24cec042fd614e78e..4d8ed39c80a92b70e98c6bc5eed2f2bfdb0c9348 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/SsoConfig.java +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/SsoConfig.java @@ -21,7 +21,7 @@ public class SsoConfig { * 构建建 SaToken redis dao(如果不需要 redis;可以注释掉) * */ @Bean - public SaTokenDao saTokenDaoInit(@Inject("${sa-token.redis}") SaTokenDaoOfRedis saTokenDao) { + public SaTokenDao saTokenDaoInit(@Inject("${sa-token.dao.redis}") SaTokenDaoOfRedis saTokenDao) { return saTokenDao; } @@ -44,12 +44,13 @@ public class SsoConfig { return SaResult.error("登录失败!"); }; - // 配置 Http 请求处理器 (在模式三的单点注销功能下用到,如不需要可以注释掉) + // 配置 Http 请求处理器 ssoServer.sendHttp = url -> { try { - // 发起 http 请求 System.out.println("------ 发起请求:" + url); - return Forest.get(url).executeAsString(); + String resStr = Forest.get(url).executeAsString(); + System.out.println("------ 请求结果:" + resStr); + return resStr; } catch (Exception e) { e.printStackTrace(); return null; diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/app.yml b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/app.yml index 2fde61a470658ed9e0229d9c1efad1efb6677ead..e895bbae2d79093f9557d78fc58a9e2d72fc90e1 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/app.yml +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/app.yml @@ -26,10 +26,10 @@ sa-token: secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # ---- 除了以上配置项,你还需要为 Sa-Token 配置http请求处理器(文档有步骤说明) -sa-token: #名字可以随意取 +sa-token.dao: #名字可以随意取 redis: server: "localhost:6379" - password: 123456 +# password: 123456 db: 1 maxTotal: 200 diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/pom.xml b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/pom.xml index 4f663aa49f00a7f5c2b20262e456a3b1b65b1eb7..5d7cea0658c47dc8ac9d4f0f8081e58cd25c2966 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/pom.xml +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/pom.xml @@ -10,13 +10,13 @@ org.noear solon-parent - 2.4.0 + 2.7.0 - 1.37.0 + 1.39.0 diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/SaConfig.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/SaConfig.java index 7bdd8aba78fbf17be5f2317b01037b6e74d800ff..6031f505f32d70212401e13236d4773621d230b9 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/SaConfig.java +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/SaConfig.java @@ -16,7 +16,7 @@ public class SaConfig { * 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) * */ @Bean - public SaTokenDao saTokenDaoInit(@Inject("${sa-token.redis}") SaTokenDaoOfRedis saTokenDao) { + public SaTokenDao saTokenDaoInit(@Inject("${sa-token.dao.redis}") SaTokenDaoOfRedis saTokenDao) { return saTokenDao; } } diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/SaSso1ClientApp.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/SaSso1ClientApp.java index c37182250981d585f04c79bfad25fbab1e2de9ca..8678b26504005604d44d3511e6851ca9973034fd 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/SaSso1ClientApp.java +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/SaSso1ClientApp.java @@ -1,6 +1,7 @@ package com.pj; +import cn.dev33.satoken.sso.SaSsoManager; import org.noear.solon.Solon; import org.noear.solon.annotation.SolonMain; @@ -15,6 +16,15 @@ public class SaSso1ClientApp { public static void main(String[] args) { Solon.start(SaSso1ClientApp.class, args); System.out.println("\nSa-Token SSO模式一 Client端启动成功"); + + System.out.println(); + System.out.println("---------------------- Solon Sa-Token SSO 模式一 Client 端启动成功 ----------------------"); + System.out.println("配置信息:" + SaSsoManager.getClientConfig()); + System.out.println("测试访问应用端一: http://s1.stp.com:9001"); + System.out.println("测试访问应用端二: http://s2.stp.com:9001"); + System.out.println("测试访问应用端三: http://s3.stp.com:9001"); + System.out.println("测试前需要根据官网文档修改hosts文件,测试账号密码:sa / 123456"); + System.out.println(); } } \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/resources/app.yml b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/resources/app.yml index 04730349b37fa5929950f66006c6a1325835ec8a..d2bb8247e1cbd753a3bcc948fb501a4f15c4375a 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/resources/app.yml +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/resources/app.yml @@ -6,16 +6,14 @@ server: sa-token: # SSO-相关配置 sso-client: - # SSO-Server端-单点登录授权地址 - auth-url: http://sso.stp.com:9000/sso/auth - # SSO-Server端-单点注销地址 - slo-url: http://sso.stp.com:9000/sso/signout + # SSO-Server端 - 主机地址 + server-url: http://sso.stp.com:9000 # 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) -sa-token: #名字可以随意取 +sa-token.dao: #名字可以随意取 redis: server: "localhost:6379" - password: 123456 +# password: 123456 db: 1 maxTotal: 200 diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/pom.xml b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/pom.xml index 91bfb5b0682086bed9ad74afde18952aace92668..d59ea9133c615bb50d11dff08d1d1ca3a07fa600 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/pom.xml +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/pom.xml @@ -10,13 +10,13 @@ org.noear solon-parent - 2.4.0 + 2.7.0 - 1.37.0 + 1.39.0 @@ -47,6 +47,13 @@ sa-token-redisx ${sa-token.version} + + + + org.noear + forest-solon-plugin + + diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/SaConfig.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/SaConfig.java index 7bdd8aba78fbf17be5f2317b01037b6e74d800ff..03b64a33991ee4860044afbfb3e99719ee597816 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/SaConfig.java +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/SaConfig.java @@ -2,8 +2,11 @@ package com.pj; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoOfRedis; +import cn.dev33.satoken.sso.config.SaSsoClientConfig; +import com.dtflys.forest.Forest; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Init; import org.noear.solon.annotation.Inject; /** @@ -16,7 +19,19 @@ public class SaConfig { * 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) * */ @Bean - public SaTokenDao saTokenDaoInit(@Inject("${sa-token.redis}") SaTokenDaoOfRedis saTokenDao) { + public SaTokenDao saTokenDaoInit(@Inject("${sa-token.dao.redis}") SaTokenDaoOfRedis saTokenDao) { return saTokenDao; } + + @Bean + public void configSso(SaSsoClientConfig ssoClient) { + // 配置Http请求处理器 + ssoClient.sendHttp = url -> { + System.out.println("------ 发起请求:" + url); + String resStr = Forest.get(url).executeAsString(); + System.out.println("------ 请求结果:" + resStr); + return resStr; + }; + } + } diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/SaSso2ClientApp.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/SaSso2ClientApp.java index 8e04700f03e6fcc454bcdc51c3fed810862f1762..a0ac5dfd66f04d1020686c90c5e8c4d11333b65d 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/SaSso2ClientApp.java +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/SaSso2ClientApp.java @@ -1,6 +1,7 @@ package com.pj; +import cn.dev33.satoken.sso.SaSsoManager; import org.noear.solon.Solon; import org.noear.solon.annotation.SolonMain; @@ -9,7 +10,15 @@ public class SaSso2ClientApp { public static void main(String[] args) { Solon.start(SaSso2ClientApp.class, args); - System.out.println("\nSa-Token SSO模式二 Client端启动成功"); + + System.out.println(); + System.out.println("---------------------- Solon Sa-Token SSO 模式二 Client 端启动成功 ----------------------"); + System.out.println("配置信息:" + SaSsoManager.getClientConfig()); + System.out.println("测试访问应用端一: http://sa-sso-client1.com:9002"); + System.out.println("测试访问应用端二: http://sa-sso-client2.com:9002"); + System.out.println("测试访问应用端三: http://sa-sso-client3.com:9002"); + System.out.println("测试前需要根据官网文档修改hosts文件,测试账号密码:sa / 123456"); + System.out.println(); } } \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/h5/H5Controller.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/h5/H5Controller.java index c169b68a7299310602b5ba7a44ada326b16eed4c..8ef49dd1a1994224ac2dd67d321f0fba51486903 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/h5/H5Controller.java +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/h5/H5Controller.java @@ -34,7 +34,7 @@ public class H5Controller implements Render { // 根据ticket进行登录 @Mapping("/sso/doLoginByTicket") public SaResult doLoginByTicket(String ticket) { - Object loginId = SaSsoClientProcessor.instance.checkTicketByMode2Or3(ticket, "/sso/doLoginByTicket"); + Object loginId = SaSsoClientProcessor.instance.checkTicket(ticket, "/sso/doLoginByTicket"); if(loginId != null) { StpUtil.login(loginId); return SaResult.data(StpUtil.getTokenValue()); diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/resources/app.yml b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/resources/app.yml index 2a4e01aba45c0cfbdaefc997be1722a667cb1710..33875b88024cff9dbb001586b09fb5deee651fb5 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/resources/app.yml +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/resources/app.yml @@ -1,22 +1,25 @@ # 端口 server: - port: 9001 + port: 9002 # sa-token配置 sa-token: # SSO-相关配置 sso-client: - # SSO-Server端 统一认证地址 - auth-url: http://sa-sso-server.com:9000/sso/auth + # SSO-Server端 主机地址 + server-url: http://sa-sso-server.com:9000 # auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html # 是否打开单点注销接口 is-slo: true + sign: + # API 接口调用秘钥 + secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) -sa-token: #名字可以随意取 +sa-token.dao: #名字可以随意取 redis: server: "localhost:6379" - password: 123456 +# password: 123456 db: 1 maxTotal: 200 diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/pom.xml b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/pom.xml index d07642aaf2d0d76fbb667082062828cad6c143ca..a9b6736e87372ca76df795a980b89055ad42ce7e 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/pom.xml +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/pom.xml @@ -10,13 +10,13 @@ org.noear solon-parent - 2.4.0 + 2.7.0 - 1.37.0 + 1.39.0 @@ -26,6 +26,12 @@ org.noear solon-api + + + + org.noear + forest-solon-plugin + @@ -47,15 +53,7 @@ sa-token-redisx ${sa-token.version} - - - - com.dtflys.forest - forest-solon-plugin - 1.5.29 - - - + diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/SaConfig.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/SaConfig.java index e02d3be5e4948be893649776115fd5f0c9128ec1..590f69f1a1eb381a13b25b9415526cf26c3068f0 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/SaConfig.java +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/SaConfig.java @@ -16,7 +16,7 @@ public class SaConfig { * 构建建 SaToken redis dao(如果不需要 redis;可以注释掉) * */ @Bean - public SaTokenDao saTokenDaoInit(@Inject("${sa-token.redis}") SaTokenDaoOfRedis saTokenDao) { + public SaTokenDao saTokenDaoInit(@Inject("${sa-token.dao.redis}") SaTokenDaoOfRedis saTokenDao) { return saTokenDao; } } diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/SaSso3ClientApp.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/SaSso3ClientApp.java index f30e4a25aba6bc184079073ed0f811b9513ab2bb..b152c52f61b021ffc351595f9eef458c63091962 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/SaSso3ClientApp.java +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/SaSso3ClientApp.java @@ -1,5 +1,6 @@ package com.pj; +import cn.dev33.satoken.sso.SaSsoManager; import org.noear.solon.Solon; import org.noear.solon.annotation.SolonMain; @@ -8,7 +9,15 @@ public class SaSso3ClientApp { public static void main(String[] args) { Solon.start(SaSso3ClientApp.class, args); - System.out.println("\nSa-Token SSO模式三 Client端启动成功"); + + System.out.println(); + System.out.println("---------------------- Solon Sa-Token SSO 模式三 Client 端启动成功 ----------------------"); + System.out.println("配置信息:" + SaSsoManager.getClientConfig()); + System.out.println("测试访问应用端一: http://sa-sso-client1.com:9003"); + System.out.println("测试访问应用端二: http://sa-sso-client2.com:9003"); + System.out.println("测试访问应用端三: http://sa-sso-client3.com:9003"); + System.out.println("测试前需要根据官网文档修改hosts文件,测试账号密码:sa / 123456"); + System.out.println(); } } \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/SaSsoAutoConfigure.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/SaSsoAutoConfigure.java deleted file mode 100644 index 2d625b4ea8458af4a4911a1b92f32381f1c0def0..0000000000000000000000000000000000000000 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/SaSsoAutoConfigure.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.pj.sso; - -import cn.dev33.satoken.sso.SaSsoManager; -import cn.dev33.satoken.sso.config.SaSsoClientConfig; -import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; -import cn.dev33.satoken.sso.template.SaSsoClientTemplate; -import org.noear.solon.annotation.Bean; -import org.noear.solon.annotation.Condition; -import org.noear.solon.annotation.Configuration; -import org.noear.solon.annotation.Inject; - -/** - * solon 的 sso 适配,在 cn.dev33:sa-token-solon-plugin:1.34.0 里还没有。(临时加这个类) - * - * //todo: 如果使用 org.noear:sa-token-solon-plugin:xxx ,则需要删掉这个类 - * - * @author noear - * @since 2.0 - */ -@Condition(onClass = SaSsoManager.class) -@Configuration -public class SaSsoAutoConfigure { - /** - * 获取 SSO 配置Bean - * */ - @Bean - public SaSsoClientConfig getConfig(@Inject(value = "${sa-token.sso-client}",required = false) SaSsoClientConfig ssoConfig) { - return ssoConfig; - } - - /** - * 注入 Sa-Token-SSO 配置Bean - * - * @param saSsoConfig 配置对象 - */ - @Bean - public void setSaSsoConfig(@Inject(required = false) SaSsoClientConfig saSsoConfig) { - SaSsoManager.setClientConfig(saSsoConfig); - } - - /** - * 注入 Sa-Token-SSO 单点登录模块 Bean - * - * @param ssoClientTemplate ssoClientTemplate对象 - */ - @Bean - public void setSaSsoClientTemplate(@Inject(required = false) SaSsoClientTemplate ssoClientTemplate) { - SaSsoClientProcessor.instance.ssoClientTemplate = ssoClientTemplate; - } -} diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/SsoConfig.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/SsoConfig.java index bb8a0a8574a7c39bd574854fb310b69ebc19a4ea..1f656d62e25e80c7e91752513d1ac416e4e2e80f 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/SsoConfig.java +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/SsoConfig.java @@ -16,7 +16,9 @@ public class SsoConfig { // 配置Http请求处理器 ssoClient.sendHttp = url -> { System.out.println("------ 发起请求:" + url); - return Forest.get(url).executeAsString(); + String resStr = Forest.get(url).executeAsString(); + System.out.println("------ 请求结果:" + resStr); + return resStr; }; } } diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/resources/app.yml b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/resources/app.yml index dff9616b0fc38d2057b8228ee9bfcd4f0123719f..7bfeb9d395671c3b0b19ed2d01aacf7bde78c431 100644 --- a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/resources/app.yml +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/resources/app.yml @@ -1,32 +1,27 @@ # 端口 server: - port: 9001 + port: 9003 # sa-token配置 sa-token: # SSO-相关配置 sso-client: - # SSO-Server端 统一认证地址 - auth-url: http://sa-sso-server.com:9000/sso/auth + # SSO-Server端 主机地址 + server-url: http://sa-sso-server.com:9000 # 使用Http请求校验ticket is-http: true - # SSO-Server端 ticket校验地址 - check-ticket-url: http://sa-sso-server.com:9000/sso/checkTicket # 打开单点注销功能 is-slo: true - # 单点注销地址 - slo-url: http://sa-sso-server.com:9000/sso/signout + sign: # 接口调用秘钥 - secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor - # SSO-Server端 查询userinfo地址 - userinfo-url: http://sa-sso-server.com:9000/sso/userinfo + secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 配置 Sa-Token Dao(此处与SSO-Server端连接不同的Redis) -sa-token: #名字可以随意取 +sa-token.dao: #名字可以随意取 redis: server: "localhost:6379" - password: 123456 +# password: 123456 db: 2 maxTotal: 200 diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/index.html b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/index.html index 755f0e0e442acad88d04224ee559967a63625aef..6f251b5448a80fe68c6c1256433629f6eb4f692f 100644 --- a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/index.html +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/index.html @@ -15,7 +15,7 @@ + + + + - - + + + @@ -213,32 +289,69 @@ + + + + + + + + - + - - - + - + @@ -388,5 +504,42 @@ $('.zk-btn--1').show(); } + + + + diff --git a/sa-token-doc/fun/auth-framework-function-test.md b/sa-token-doc/fun/auth-framework-function-test.md new file mode 100644 index 0000000000000000000000000000000000000000..3625d6665ac8e9197a5fff89ace7a8620a63832d --- /dev/null +++ b/sa-token-doc/fun/auth-framework-function-test.md @@ -0,0 +1,1955 @@ +# Java 权限认证框架功能 测试 / 对比 / 迁移。 + +对比以下框架的常见功能,为项目技术栈迁移提供代码示例 + +- Sa-Token +- Apache Shiro +- Spring Security +- JWT + + +> [!TIP| label:注意事项] +> - 因个人精力&能力有限,本篇只展示部分常见功能的对比,也欢迎大家一起贡献案例,提交pr。 +> - 代码案例仓库:[https://gitee.com/sa-tokens/auth-framework-function-test](https://gitee.com/sa-tokens/auth-framework-function-test) +> - 注:本篇主要展示一些常见功能不同框架的实现差异,而非每个框架的所含功能点对比。 + + +--- + + +### 依赖引入 + + + + +``` xml + + + cn.dev33 + sa-token-spring-boot3-starter + 1.39.0 + +``` + + +``` xml + + + org.apache.shiro + shiro-spring-boot-web-starter + 1.13.0 + +``` + + +``` xml + + + org.springframework.boot + spring-boot-starter-security + 3.3.2 + +``` +SpringBoot 项目下一般不用特别指定 SpringSecurity 版本号 + + +``` xml + + + cn.hutool + hutool-all + 5.8.29 + +``` + + + + + + +### 会话登录 & 会话状态查询 + + + + +测试 Controller +``` java +@RestController +@RequestMapping("/acc/") +public class LoginController { + + @Autowired + SysUserDao sysUserDao; + + // 测试登录 + @RequestMapping("doLogin") + public AjaxJson doLogin(String username, String password) { + // 校验 + SysUser user = sysUserDao.findByUsername(username); + if(user == null) { + return AjaxJson.getError("用户不存在"); + } + if(!user.getPassword().equals(password)) { + return AjaxJson.getError("密码错误"); + } + // 登录 + StpUtil.login(user.getId()); + StpUtil.getSession().set("user", user); + return AjaxJson.getSuccess("登录成功"); + } + + // 查询登录状态 + @RequestMapping("isLogin") + public AjaxJson isLogin() { + if(StpUtil.isLogin()) { + return AjaxJson.getSuccess("已登录,账号id:" + StpUtil.getLoginId()); + } + return AjaxJson.getError("未登录"); + } + +} +``` + + +自定义 Realm +``` java +public class MyRealm extends AuthorizingRealm { + + @Autowired + private SysUserDao sysUserDao; + + // 加载用户信息 + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { + String username = (String)token.getPrincipal(); + SysUser sysUser = sysUserDao.findByUsername(username); + if(sysUser == null){ + return null; + } + return new SimpleAuthenticationInfo( + sysUser, + sysUser.getPassword(), + getName() + ); + } + + // 加载权限信息 + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { + return null; + } + +} +``` + +Shiro 配置类 +``` java +@Configuration +public class ShiroConfigure { + + @Bean + public MyRealm myRealm() { + return new MyRealm(); + } + + @Bean + public DefaultWebSecurityManager securityManager() { + DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); + manager.setRealm(myRealm()); + return manager; + } + + @Bean + public ShiroFilterFactoryBean shiroFilterFactoryBean() { + ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); + bean.setSecurityManager(securityManager()); + return bean; + } + +} +``` + +测试 Controller +``` java +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8082/acc/doLogin?username=zhang&password=123456 + @RequestMapping("doLogin") + public AjaxJson doLogin(String username, String password) { + Subject subject = SecurityUtils.getSubject(); + try { + subject.login(new UsernamePasswordToken(username, password)); + return AjaxJson.getSuccess("登录成功!"); + } catch (AuthenticationException e) { + e.printStackTrace(); + return AjaxJson.getError(e.getMessage()); + } + } + + // 查询登录状态 ---- http://localhost:8082/acc/isLogin + @RequestMapping("isLogin") + public AjaxJson isLogin() { + Subject subject = SecurityUtils.getSubject(); + if(subject.isAuthenticated()) { + SysUser sysUser = (SysUser)subject.getPrincipal(); + return AjaxJson.getSuccess("已登录,账号id:" + sysUser.getId()); + } + return AjaxJson.getError("未登录"); + } + +} +``` + + + +定义 SpringSecurity 配置类 +``` java +@Configuration +public class SpringSecurityConfigure { + + /** + * Spring Security的核心过滤器链配置 + */ + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + // 定义安全请求拦截规则 + httpSecurity.authorizeHttpRequests(router -> { + router + // 放行接口 + .requestMatchers("/acc/doLogin", "/acc/isLogin").permitAll() + + // 所有请求都需要认证 + .anyRequest().authenticated(); + ; + }); + + // 默认的表单登录 + httpSecurity.formLogin(withDefaults()); + + // 是否启用 csrf 防御 + httpSecurity.csrf( csrf -> csrf.disable() ); + + // 一些安全相关的全局响应头 + httpSecurity.headers(httpSecurityHeadersConfigurer -> { + httpSecurityHeadersConfigurer.cacheControl(HeadersConfigurer.CacheControlConfig::disable); + httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable); + }); + + return httpSecurity.build(); + } + + /** + * Spring Security 认证管理器 + */ + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + +} +``` + +定义 SpringSecurity UserDetails 管理器 +``` java +/** + * 自定义 SpringSecurity UserDetails 管理器 + * + * @author click33 + * @since 2024/8/8 + */ +@Component +public class CustomUserDetailsManager implements UserDetailsManager { + + @Autowired + SysUserDao sysUserDao; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + SysUser sysUser = sysUserDao.findByUsername(username); + if(sysUser == null){ + throw new UsernameNotFoundException("用户不存在"); + } + return User.withUsername(sysUser.getUsername()) + .password("{noop}" + sysUser.getPassword()) + .build(); + } + + @Override + public void createUser(UserDetails user) { + + } + + @Override + public void updateUser(UserDetails user) { + + } + + @Override + public void deleteUser(String username) { + + } + + @Override + public void changePassword(String oldPassword, String newPassword) { + + } + + @Override + public boolean userExists(String username) { + return false; + } + +} +``` + +测试 Controller + +``` java +@RestController +@RequestMapping("/acc/") +public class LoginController { + + @Autowired + AuthenticationManager authenticationManager; + + @Autowired + SysUserDao sysUserDao; + + // 测试登录 ---- http://localhost:8083/acc/doLogin?username=zhang&password=123456 + @RequestMapping("doLogin") + public AjaxJson doLogin(String username, String password, HttpServletRequest request) { + try { + // 验证账号密码 + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password); + usernamePasswordAuthenticationToken.setDetails(sysUserDao.findByUsername(username)); + Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken); + // 存入上下文 + SecurityContextHolder.getContext().setAuthentication(authentication); + request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext()); + // 返回 + return AjaxJson.getSuccess("登录成功!"); + } catch (Exception e) { + e.printStackTrace(); + return AjaxJson.getError(e.getMessage()); + } + } + + // 查询登录状态 ---- http://localhost:8083/acc/isLogin + @RequestMapping("isLogin") + public AjaxJson isLogin() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return AjaxJson.getSuccess("是否登录:" + !(authentication instanceof AnonymousAuthenticationToken)) + .set("principal", authentication.getPrincipal()) + .set("details", authentication.getDetails()); + } + +} + +``` + + + +测试 Controller +``` java +@RestController +@RequestMapping("/acc/") +public class LoginController { + + @Autowired + SysUserDao sysUserDao; + + // 测试登录 + @RequestMapping("doLogin") + public AjaxJson doLogin(String username, String password) { + // 校验 + SysUser user = sysUserDao.findByUsername(username); + if(user == null) { + return AjaxJson.getError("用户不存在"); + } + if(!user.getPassword().equals(password)) { + return AjaxJson.getError("密码错误"); + } + // 登录 + String token = JwtUtil.createToken(user.getId(), user, 60 * 60 * 2); + return AjaxJson.getSuccess("登录成功").set("token", token); + } + + // 查询登录状态 + @RequestMapping("isLogin") + public AjaxJson isLogin(HttpServletRequest request) { + try{ + String token = request.getHeader("token"); + JWT jwt = JwtUtil.parseToken(token); + return AjaxJson.getSuccess("已登录") + .set("id", jwt.getPayload("userId")) + .set("user", jwt.getPayload("user")); + } catch (Exception e) { + e.printStackTrace(); + return AjaxJson.getError("未登录"); + } + } + +} +``` + + + + + + +### 会话注销 + + + + +``` java +@RequestMapping("logout") +public AjaxJson logout() { + StpUtil.logout(); + return AjaxJson.getSuccess("注销成功"); +} +``` + + + +``` java +@RequestMapping("logout") +public AjaxJson logout() { + SecurityUtils.getSubject().logout(); + return AjaxJson.getSuccess("注销成功"); +} +``` + + +``` java +@Bean +public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + + // 其它配置 ... + + // 注销相关配置 + httpSecurity.logout(logout -> { + logout.logoutUrl("/acc/logout"); + logout.logoutSuccessHandler((request, response, authentication) -> { + response.setStatus(200); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json; charset=utf-8"); + String jsonStr = new ObjectMapper().writeValueAsString(AjaxJson.getSuccess("注销成功!")); + response.getWriter().write(jsonStr); + }); + }); + + return httpSecurity.build(); +} +``` + + +JWT 无法注销已经颁发的 token 。 + + + + + +### 账号密码登录(MD5 加 salt) + + + + +测试 Controller +``` java +@RequestMapping("doLogin") +public AjaxJson doLogin(String username, String password) { + // 校验 + SysUser user = sysUserDao.findByUsername(username); + if(user == null) { + return AjaxJson.getError("用户不存在"); + } + String salt = "abc"; + if(!user.getPassword().equals(SaSecureUtil.md5(salt + password))) { + return AjaxJson.getError("密码错误"); + } + // 登录 + StpUtil.login(user.getId()); + StpUtil.getSession().set("user", user); + return AjaxJson.getSuccess("登录成功"); +} +``` + + +自定义 Realm Bean 设定密码凭证器 +``` java +@Bean +public MyRealm myRealm() { + MyRealm realm = new MyRealm(); + // 设定凭证匹配器 + HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); + credentialsMatcher.setHashAlgorithmName("md5"); + realm.setCredentialsMatcher(credentialsMatcher); + // 返回 + return realm; +} +``` + +自定义 Realm 实现类 doGetAuthenticationInfo 方法返回 slat 信息 +``` java +// 加载用户信息 +@Override +protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { + String username = (String)token.getPrincipal(); + SysUser sysUser = sysUserDao.findByUsername(username); + if(sysUser == null){ + return null; + } + + return new SimpleAuthenticationInfo( + sysUser, + sysUser.getPassword(), + ByteSource.Util.bytes("abc"), // 指定 slat 信息 + getName() + ); +} +``` + +登录代码照旧 + + + +CustomUserDetailsManager 的 loadUserByUsername 指定 MD5 算法 + +``` java +@Override +public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + SysUser sysUser = sysUserDao.findByUsername(username); + if(sysUser == null){ + throw new UsernameNotFoundException("用户不存在"); + } + return User.withUsername(sysUser.getUsername()) + .password("{MD5}" + sysUser.getPassword()) + .build(); +} +``` + +登录时指定 salt + +``` java +@RequestMapping("doLogin") +public AjaxJson doLogin(String username, String password, HttpServletRequest request) { + try { + // 验证账号密码 + String salt = "abc"; + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, salt + password); + // 其它代码照旧 ... + + // 返回 + return AjaxJson.getSuccess("登录成功!"); + } catch (Exception e) { + e.printStackTrace(); + return AjaxJson.getError(e.getMessage()); + } +} +``` + + + +测试 Controller +``` java +@RequestMapping("doLogin") +public AjaxJson doLogin(String username, String password) { + // 校验 + SysUser user = sysUserDao.findByUsername(username); + if(user == null) { + return AjaxJson.getError("用户不存在"); + } + String salt = "abc"; + if(!user.getPassword().equals(SecureUtil.md5(salt + password))) { + return AjaxJson.getError("密码错误"); + } + // 登录 + String token = JwtUtil.createToken(user.getId(), user, 60 * 60 * 2); + return AjaxJson.getSuccess("登录成功").set("token", token); +} +``` + + + + + +### 从上下文获取当前登录 User 信息 + + +``` java +// 从上下文获取当前登录 User 信息 +@RequestMapping("getCurrUser") +public AjaxJson getCurrUser() { + return AjaxJson.getSuccess() + .set("id", StpUtil.getLoginId()) + .set("user", StpUtil.getSession().get("user")); +} +``` + + + +``` java +// 从上下文获取当前登录 User 信息 +@RequestMapping("getCurrUser") +public AjaxJson getCurrUser() { + Subject subject = SecurityUtils.getSubject(); + SysUser sysUser = (SysUser)subject.getPrincipal(); + return AjaxJson.getSuccess() + .set("id", sysUser.getId()) + .set("user", sysUser); +} +``` + + +``` java +// 从上下文获取当前登录 User 信息 +@RequestMapping("getCurrUser") +public AjaxJson getCurrUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if(!(authentication instanceof AnonymousAuthenticationToken)) { + SysUser sysUser = (SysUser)authentication.getDetails(); + return AjaxJson.getSuccess() + .set("id", sysUser.getId()) + .set("user", sysUser); + } + return AjaxJson.getError("未登录"); +} +``` + + +``` java +// 从上下文获取当前登录 User 信息 +@RequestMapping("getCurrUser") +public AjaxJson getCurrUser(HttpServletRequest request) { + try{ + String token = request.getHeader("token"); + JWT jwt = JwtUtil.parseToken(token); + SysUser sysUser = jwt.getPayloads().get("user", SysUser.class); + return AjaxJson.getSuccessData(sysUser); + } catch (Exception e) { + e.printStackTrace(); + return AjaxJson.getError("未登录"); + } +} +``` + + + + + +### 从会话上下文上存取值 + + +``` java +// 测试从从会话上下文存取值 +@RequestMapping("testSession") +public AjaxJson test() { + SaSession session = StpUtil.getSession(); + + System.out.println("从 session 上取值:" + session.get("name")); + session.set("name", "zhang"); + System.out.println("从 session 上取值:" + session.get("name")); + + return AjaxJson.getSuccess(); +} +``` + + + +``` java +// 测试从从会话上下文存取值 +@RequestMapping("testSession") +public AjaxJson test() { + Subject subject = SecurityUtils.getSubject(); + Session session = subject.getSession(); + + System.out.println("从 session 上取值:" + session.getAttribute("name")); + session.setAttribute("name", "zhang"); + System.out.println("从 session 上取值:" + session.getAttribute("name")); + + return AjaxJson.getSuccess(); +} +``` + + + +``` java +// 测试从从会话上下文存取值 +@RequestMapping("testSession") +public AjaxJson testSession(HttpServletRequest request) { + HttpSession session = request.getSession(); + + System.out.println("从 session 上取值:" + session.getAttribute("name")); + session.setAttribute("name", "zhang"); + System.out.println("从 session 上取值:" + session.getAttribute("name")); + return AjaxJson.getSuccess(); +} +``` + + +无 + + + + + + + + + +### 角色认证 & 权限认证 + + + + +自定义 StpInterface 实现类 + +``` java +@Component +public class StpInterfaceImpl implements StpInterface { + + // 加载角色信息 + @Override + public List getPermissionList(Object loginId, String loginType) { + return Arrays.asList("admin", "super-admin", "ceo"); + } + + // 加载权限信息 + @Override + public List getRoleList(Object loginId, String loginType) { + return Arrays.asList("user:add", "user:delete", "user:update"); + } + +} +``` + +测试 Controller +``` java +@RestController +@RequestMapping("/jur/") +public class JurController { + + // 角色判断 ---- http://localhost:8082/jur/assertRole + @RequestMapping("assertRole") + public AjaxJson assertRole() { + // is 模式,返回 true 或 false + System.out.println("单个角色判断:" + StpUtil.hasRole("admin")); + System.out.println("多个角色判断(and):" + StpUtil.hasRoleAnd("admin", "dev-admin")); + System.out.println("多个角色判断(or):" + StpUtil.hasRoleOr("admin", "dev-admin")); + + // check 模式,无角色时抛出异常 + StpUtil.checkRole("admin"); // 单个 check + StpUtil.checkRoleAnd("admin", "dev-admin"); // 多个 check (and) + StpUtil.checkRoleOr("admin", "dev-admin"); // 多个 check (or) + + return AjaxJson.getSuccess(); + } + + // 权限判断 ---- http://localhost:8082/jur/assertPermission + @RequestMapping("assertPermission") + public AjaxJson assertPermission() { + // is 模式,返回 true 或 false + System.out.println("单个权限判断:" + StpUtil.hasPermission("user:add")); + System.out.println("多个权限判断(and):" + StpUtil.hasPermissionAnd("user:add", "user:delete22")); + System.out.println("多个权限判断(or):" + StpUtil.hasPermissionOr("user:add", "user:delete22")); + + // check 模式,无权限时抛出异常 + StpUtil.checkPermission("user:add"); // 单个 check + StpUtil.checkPermissionAnd("user:add", "user:delete22"); // 多个 check (and) + StpUtil.checkPermissionOr("user:add", "user:delete22"); // 多个 check (or) + + return AjaxJson.getSuccess(); + } + +} +``` + + + + +自定义 Realm 里重写方法 doGetAuthorizationInfo + +``` java +@Override +protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { + SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); + // 加载角色信息 + authorizationInfo.addRoles(Arrays.asList("admin", "super-admin", "ceo")); + // 加载权限信息 + authorizationInfo.addStringPermissions(Arrays.asList("user:add", "user:delete", "user:update")); + return authorizationInfo; +} +``` + +测试 Controller +``` java +@RestController +@RequestMapping("/jur/") +public class JurController { + + // 角色判断 + @RequestMapping("assertRole") + public AjaxJson assertRole() { + Subject subject = SecurityUtils.getSubject(); + + // is 模式,返回 true 或 false + System.out.println("单个角色判断:" + subject.hasRole("admin")); + System.out.println("多个角色判断(and):" + subject.hasAllRoles(Arrays.asList("admin", "dev-admin"))); + System.out.println("多个角色判断(or):" + (subject.hasRole("admin") || subject.hasRole("dev-admin"))); + + // check 模式,无角色时抛出异常 + subject.checkRole("admin"); // 单个 check + subject.checkRoles("admin", "dev-admin"); // 多个 check (and) + + return AjaxJson.getSuccess(); + } + + // 权限判断 + @RequestMapping("assertPermission") + public AjaxJson assertPermission() { + Subject subject = SecurityUtils.getSubject(); + + // is 模式,返回 true 或 false + System.out.println("单个权限判断:" + subject.isPermitted("user:add")); + System.out.println("多个权限判断(and):" + subject.isPermittedAll("user:add", "user:delete22")); + System.out.println("多个权限判断(or):" + (subject.isPermitted("user:add") || subject.isPermitted("user:delete22"))); + + // check 模式,无权限时抛出异常 + subject.checkPermission("user:add"); // 单个 check + subject.checkPermissions("user:add", "user:delete22"); // 多个 check (and) + + return AjaxJson.getSuccess(); + } + +} +``` + + + +CustomUserDetailsManager 的 loadUserByUsername 里返回用户的 角色 或 权限 信息 +``` java +@Override +public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + SysUser sysUser = sysUserDao.findByUsername(username); + if(sysUser == null){ + throw new UsernameNotFoundException("用户不存在"); + } + + // 不可以同时返回 roles 和 authorities,因为会相互覆盖,SpringSecurity 源码有bug + return User.withUsername(sysUser.getUsername()) + .password("{noop}" + sysUser.getPassword()) + // .roles("admin", "super-admin", "ceo") + .authorities("user:add", "user:delete", "user:update") + .build(); +} +``` + +测试 Controller +``` java +@RestController +@RequestMapping("/jur/") +public class JurController { + + // 角色判断 + @RequestMapping("assertRole") + public AjaxJson assertRole() { + SecurityExpressionRoot securityExpressionRoot = new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {}; + + System.out.println("单个角色判断:" + securityExpressionRoot.hasRole("admin")); + System.out.println("多个角色判断(and):" + (securityExpressionRoot.hasRole("admin") && securityExpressionRoot.hasRole("dev-admin"))); + System.out.println("多个角色判断(or):" + securityExpressionRoot.hasAnyRole("admin", "dev-admin")); + + return AjaxJson.getSuccess(); + } + + // 权限判断 + @RequestMapping("assertPermission") + public AjaxJson assertPermission() { + SecurityExpressionRoot securityExpressionRoot = new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {}; + + System.out.println("单个权限判断:" + securityExpressionRoot.hasAuthority("user:add")); + System.out.println("多个权限判断(and):" + (securityExpressionRoot.hasAuthority("user:add") && securityExpressionRoot.hasAuthority("user:delete2"))); + System.out.println("多个权限判断(or):" + securityExpressionRoot.hasAnyAuthority("user:add", "user:delete2")); + + return AjaxJson.getSuccess(); + } + +} +``` + + +无 + + + + + + +### 注解鉴权 + + + + +SaTokenConfigure 配置注解拦截器 +``` java +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); + } +} +``` + +测试 Controller +``` java +@RestController +@RequestMapping("/at-check/") +public class AtCheckController { + + // 登录校验 + @SaCheckLogin + @RequestMapping("checkLogin") + public AjaxJson checkLogin() { + return AjaxJson.getSuccess(); + } + + // 角色校验 + @SaCheckRole("admin") + @RequestMapping("checkRole") + public AjaxJson checkRole() { + return AjaxJson.getSuccess(); + } + + // 权限校验 + @SaCheckPermission("user:add") + @RequestMapping("checkPermission") + public AjaxJson checkPermission() { + return AjaxJson.getSuccess(); + } + + // 忽略认证校验 + @SaIgnore + @SaCheckLogin + @RequestMapping("ignoreCheck") + public AjaxJson ignoreCheck() { + return AjaxJson.getSuccess(); + } + +} +``` + + + + +测试 Controller +``` java +@RestController +@RequestMapping("/at-check/") +public class AtCheckController { + + // 登录校验 + @RequiresAuthentication + @RequestMapping("checkLogin") + public AjaxJson checkLogin() { + return AjaxJson.getSuccess(); + } + + // 角色校验 + @RequiresRoles("admin") + @RequestMapping("checkRole") + public AjaxJson checkRole() { + return AjaxJson.getSuccess(); + } + + // 权限校验 + @RequiresPermissions("user:add") + @RequestMapping("checkPermission") + public AjaxJson checkPermission() { + return AjaxJson.getSuccess(); + } + +} +``` + + + +`SpringSecurityConfigure` 配置类加上 `@EnableMethodSecurity` 注解 +``` java +@Configuration +@EnableMethodSecurity +public class SpringSecurityConfigure { + // ... +} +``` + +测试 Controller +``` java +@RestController +@RequestMapping("/at-check/") +public class AtCheckController { + + // 登录校验 + @PreAuthorize("isAuthenticated()") + @RequestMapping("checkLogin") + public AjaxJson checkLogin() { + return AjaxJson.getSuccess(); + } + + // 角色校验 + @PreAuthorize("hasRole('admin')") + @RequestMapping("checkRole") + public AjaxJson checkRole() { + return AjaxJson.getSuccess(); + } + + // 权限校验 + @PreAuthorize("hasAuthority('user:add')") + @RequestMapping("checkPermission") + public AjaxJson checkPermission() { + return AjaxJson.getSuccess(); + } + +} +``` + + +无 + + + + + + +### 路由拦截鉴权 + + + +SaTokenConfigure 配置 +``` java +@Override +public void addInterceptors(InterceptorRegistry registry) { + // 注册 Sa-Token 拦截器打开注解鉴权功能 + registry.addInterceptor(new SaInterceptor(handle -> { + SaRouter.match("/route-check/getInfo1").stop(); // 不拦截 + SaRouter.match("/route-check/getInfo2").check(r -> StpUtil.checkLogin()); // 需要登录 + SaRouter.match("/route-check/getInfo3").check(r -> StpUtil.checkRole("admin2")); // 需要角色 + SaRouter.match("/route-check/getInfo4").check(r -> StpUtil.checkPermission("user:add3")); // 需要权限 + })).addPathPatterns("/**"); +} +``` + + + +过滤器配置 +``` java +@Bean +public ShiroFilterFactoryBean shiroFilterFactoryBean() { + ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); + bean.setSecurityManager(securityManager()); + + // 路由拦截鉴权 + Map filterMap = new LinkedHashMap<>(); + filterMap.put("/route-check/getInfo", "anon"); // 不拦截 + filterMap.put("/route-check/getInfo2", "authc"); // 需要登录 + filterMap.put("/route-check/getInfo3", "perms[admin2]"); // 需要角色 + filterMap.put("/route-check/getInfo4", "perms[user:add3]"); // 需要权限 + bean.setFilterChainDefinitionMap(filterMap); + bean.setLoginUrl("/401"); // 未登录时跳转的 url + bean.setUnauthorizedUrl("/403"); // 未授权时跳转的 url + + return bean; +} +``` + + +SpringSecurityConfigure 配置 + +``` java +@Bean +public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + // 定义安全请求拦截规则 + httpSecurity.authorizeHttpRequests(router -> { + router + .requestMatchers("/route-check/getInfo1").permitAll() // 不拦截 + .requestMatchers("/route-check/getInfo2").authenticated() // 需要登录 + .requestMatchers("/route-check/getInfo3").hasRole("admin") // 需要 admin 角色 + .requestMatchers("/route-check/getInfo4").hasAuthority("user:add") // 需要 user:add 权限 + .anyRequest().permitAll(); // 所有请求都放行 + }); + + return httpSecurity.build(); +} +``` + + +无 + + + + + +### 鉴权未通过的处理方案 + + + +定义全局异常处理类 +``` java +@RestControllerAdvice +public class GlobalException { + + @ExceptionHandler(NotLoginException.class) + public AjaxJson handlerException(NotLoginException e) { + return AjaxJson.get(401, "未登录"); + } + + @ExceptionHandler(NotRoleException.class) + public AjaxJson handlerException(NotRoleException e) { + return AjaxJson.get(403, "缺少角色:" + e.getRole()); + } + + @ExceptionHandler(NotPermissionException.class) + public AjaxJson handlerException(NotPermissionException e) { + return AjaxJson.get(403, "缺少权限:" + e.getPermission()); + } + +} +``` + + + +过滤器配置 +``` java +@Bean +public ShiroFilterFactoryBean shiroFilterFactoryBean() { + ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); + + // ... + + bean.setLoginUrl("/401"); // 未登录时跳转的 url + bean.setUnauthorizedUrl("/403"); // 未授权时跳转的 url + + return bean; +} +``` + +定义路由 +``` java +@RestController +public class ShiroErrorController { + + @RequestMapping("/401") + public Object error401(HttpServletRequest request, HttpServletResponse response) { + response.setStatus(200); + return AjaxJson.get(401, "not login"); + } + + @RequestMapping("/403") + public Object error403(HttpServletRequest request, HttpServletResponse response) { + response.setStatus(200); + return AjaxJson.get(403, "鉴权未通过"); + } + +} +``` + + + +实现 `AccessDeniedHandler`, `AuthenticationEntryPoint` 接口 + +``` java +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler, AuthenticationEntryPoint, Serializable { + + // 未登录异常 + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + //验证为未登陆状态会进入此方法,认证错误 + response.setStatus(401); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json; charset=utf-8"); + PrintWriter printWriter = response.getWriter(); + String body = "请先进行登录"; + printWriter.write(body); + printWriter.flush(); + } + + // 权限不足 + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { + // 登陆状态下,权限不足执行该方法 + response.setStatus(200); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json; charset=utf-8"); + PrintWriter printWriter = response.getWriter(); + String body = "权限不足"; + printWriter.write(body); + printWriter.flush(); + } +} +``` + +注入 `SecurityFilterChain` + +``` java +// 未登录处理逻辑、权限不足处理逻辑 +@Autowired +private CustomAccessDeniedHandler accessDeniedHandler; + +@Bean +public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + + // 异常处理 + httpSecurity.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> { + // 权限不足处理方案 + httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(accessDeniedHandler); + // 未登录 处理逻辑 + httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(accessDeniedHandler); + }); + + return httpSecurity.build(); +} +``` + + +使用 `try-catch` 捕获,或定义全局异常处理 +``` java +@RestControllerAdvice +public class GlobalException { + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) { + + // 打印堆栈,以供调试 + System.out.println("全局异常---------------"); + e.printStackTrace(); + + // 返回给前端 + return AjaxJson.getError(e.getMessage()); + } +} +``` + + + + + + + +### 和 Thymeleaf 集成 + + + + +`pom.xml` 依赖 +``` xml + + + cn.dev33 + sa-token-dialect-thymeleaf + ${sa-token.version} + +``` + +`SaTokenConfigure` 增加配置 `Sa-Token` 标签方言对象 + +``` java +// Sa-Token 标签方言 (Thymeleaf版) +@Bean +public SaTokenDialect getSaTokenDialect() { + return new SaTokenDialect(); +} +``` + +新建 `ThymeleafConfigure` 注入全局变量 +``` java +@Configuration +public class ThymeleafConfigure { + // 为 Thymeleaf 注入全局变量,以便在页面中调用 Sa-Token 的方法 + @Autowired + public void configureThymeleafStaticVars(ThymeleafViewResolver viewResolver) { + viewResolver.addStaticVariable("stp", StpUtil.stpLogic); + } +} +``` + +新建 `Controller` +``` java +@Controller +public class HomeController { + @RequestMapping("/") + public Object index(HttpServletRequest request) { + request.setAttribute("isLogin", StpUtil.isLogin()); + return new ModelAndView("index.html"); + } +} +``` + +新建 `templates/index.html` +``` html + + + + Sa-Token 集成 Thymeleaf 标签方言 + + + + +
+

Sa-Token 集成 Thymeleaf 标签方言 —— 测试页面

+

当前是否登录:

+

+ 登录 + 注销 +

+ +

登录之后才能显示:value

+

不登录才能显示:value

+ +

具有角色 admin 才能显示:value

+

同时具备多个角色才能显示:value

+

只要具有其中一个角色就能显示:value

+

不具有角色 admin 才能显示:value

+ +

具有权限 user-add 才能显示:value

+

同时具备多个权限才能显示:value

+

只要具有其中一个权限就能显示:value

+

不具有权限 user-add 才能显示:value

+ +

+ 从SaSession中取值: + +

+ +
+ + +``` + + + + +`pom.xml` 依赖 +``` xml + + + com.github.theborakompanioni + thymeleaf-extras-shiro + 2.1.0 + +``` + +`ShiroConfigure` 增加配置 `Shiro` 方言对象 +``` java +@Bean +public ShiroDialect shiroDialect() { + return new ShiroDialect(); +} +``` + +新建 `Controller` +``` java +@Controller +public class HomeController { + @RequestMapping("/") + public Object index(HttpServletRequest request) { + Subject subject = SecurityUtils.getSubject(); + request.setAttribute("isLogin", subject.isAuthenticated()); + return new ModelAndView("index.html"); + } +} +``` + +新建 `templates/index.html` + +``` html + + + + Shiro 集成 Thymeleaf 标签方言 + + + + +
+

Shiro 集成 Thymeleaf 标签方言 —— 测试页面

+

当前是否登录:

+

+ 登录 + 注销 +

+

登录之后才能显示:value

+

不登录才能显示:value

+ +

具有角色 admin 才能显示:value

+

同时具备多个角色才能显示:value

+

只要具有其中一个角色就能显示:value

+

不具有角色 admin 才能显示:value

+ +

具有权限 user-add 才能显示:value

+

同时具备多个权限才能显示:value

+

只要具有其中一个权限就能显示:value

+

不具有权限 user-add 才能显示:value

+ +

+ 当前登录账号: +

+ +
+ + +``` + + + +`pom.xml` 引入依赖 +``` xml + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity6 + 3.1.2.RELEASE + +``` + +新建 `Controller` +``` java +@RestController +public class HomeController { + // 首页 + @RequestMapping("/") + public Object index(HttpServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + request.setAttribute("isLogin", !(authentication instanceof AnonymousAuthenticationToken)); + return new ModelAndView("index.html"); + } +} +``` + +新建 `templates/index.html` + +``` html + + + + Shiro 集成 Thymeleaf 标签方言 + + + + +
+

Shiro 集成 Thymeleaf 标签方言 —— 测试页面

+

当前是否登录:

+ +

+ 登录 + 注销 +

+

登录之后才能显示:value

+

不登录才能显示:value

+ +

具有角色 admin 才能显示:value

+

同时具备多个角色才能显示:value

+

只要具有其中一个角色就能显示:value

+

不具有角色 admin 才能显示:value

+ +

具有权限 user-add 才能显示:value

+

同时具备多个权限才能显示:value

+

只要具有其中一个权限就能显示:value

+

不具有权限 user-add 才能显示:value

+ +

+ 当前登录账号: +

+ +
+ + +``` + + +无 + + + + + + +### 前后端分离 + + +1、在登录时,将 token 信息返回到前端 +``` java +// 测试登录 +@RequestMapping("doLogin") +public AjaxJson doLogin(String username, String password) { + // 校验 + SysUser user = sysUserDao.findByUsername(username); + // user 信息校验代码不再赘述 ... + + // 登录 + StpUtil.login(user.getId()); + StpUtil.getSession().set("user", user); + return AjaxJson.getSuccess("登录成功").set("satoken", StpUtil.getTokenValue()); // 关键代码 +} +``` + +2、前端改造 +- 1、在登录请求时,将返回的 token 保存到本地 `localStorage.setItem('satoken', res.satoken)`。 +- 2、在后续每次请求中,读取本地保存的 satoken 塞到请求 header 中 + +``` js +const header = {}; +if(localStorage.satoken) { + header.satoken = localStorage.satoken; +} +// 后续提交请求... +``` + + + + +1、自定义 SessionManager,从请求 header 里读取前端提交的 token +``` java +public class MySessionManager extends DefaultWebSessionManager { + + private static final String TOKEN = "token"; + + private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; + + public MySessionManager() { + super(); + } + + @Override + protected Serializable getSessionId(ServletRequest request, ServletResponse response) { + String id = WebUtils.toHttp(request).getHeader(TOKEN); + // 如果请求头中有 token 则其值为sessionId + if (!StringUtils.isEmpty(id)) { + request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); + request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); + request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); + return id; + } else { + //否则按默认规则从cookie取sessionId + return super.getSessionId(request, response); + } + } +} +``` + +2、注入到 SecurityManager 中 +``` java +@Configuration +public class ShiroConfigure { + + // 省略其它次要代码 ... + + @Bean + public DefaultWebSecurityManager securityManager() { + DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); + manager.setRealm(myRealm()); + manager.setSessionManager(sessionManager()); + return manager; + } + + @Bean + public ShiroFilterFactoryBean shiroFilterFactoryBean() { + ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); + bean.setSecurityManager(securityManager()); + return bean; + } + + // 自定义sessionManager + @Bean + public SessionManager sessionManager() { + MySessionManager mySessionManager = new MySessionManager(); + return mySessionManager; + } +} +``` + +3、测试 Controller,登录时将 token 信息返回到前端 +``` java +// 测试登录 +@RequestMapping("doLogin") +public AjaxJson doLogin(String username, String password) { + Subject subject = SecurityUtils.getSubject(); + try { + subject.login(new UsernamePasswordToken(username, password)); + String token = subject.getSession().getId().toString(); // 关键代码 + return AjaxJson.getSuccess("登录成功!").set("token", token); // 关键代码 + } catch (AuthenticationException e) { + e.printStackTrace(); + return AjaxJson.getError(e.getMessage()); + } +} +``` + +4、前端改造 +- 1、在登录请求时,将返回的 token 保存到本地 `localStorage.setItem('token', res.token)`。 +- 2、在后续每次请求中,读取本地保存的 token 塞到请求 header 中 + +``` js +const header = {}; +if(localStorage.token) { + header.token = localStorage.token; +} +// 后续提交请求... +``` + + + +见下方 “集成 Redis” 部分,同时做到:集成 Redis + 前后端分离。 + + + +`JWT` 不依赖 `Cookie` 保存/传输 token,因此无需特殊定制即可原生支持前后端分离模式。 + + + + + + +### 集成 Redis + + + + +pom.xml 引入依赖 +``` xml + + + cn.dev33 + sa-token-redis-jackson + ${sa-token.version} + + + + + org.apache.commons + commons-pool2 + +``` + +application.yml 新增连接配置 +``` yaml +spring: + data: + # 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 +``` + +其它代码照旧 + + + +pom.xml 引入依赖 + +``` xml + + + org.crazycake + shiro-redis + 3.3.1 + +``` + +application.yml 新增连接配置 +``` yaml + +spring: + redis: + shiro: + # Redis服务器地址 + host: 127.0.0.1:6379 + # Redis服务器连接密码(默认为空) + password: + # Redis数据库索引(默认为0) + database: 2 + # 连接超时时间 + timeout: 1800 + +``` + + +ShiroConfigure 注入相关 Bean +``` java +@Configuration +public class ShiroConfigure { + + // 自定义 securityManager + @Bean + public DefaultWebSecurityManager securityManager() { + DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); + // manager.setRealm(myRealm()); + + // 自定义session管理 使用redis + manager.setSessionManager(sessionManager()); + // 自定义缓存实现 使用redis + manager.setCacheManager(cacheManager()); + + return manager; + } + + @Bean + public ShiroFilterFactoryBean shiroFilterFactoryBean() { + ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); + bean.setSecurityManager(securityManager()); + return bean; + } + + // -------- 以下为 shiro redis 相关 -------- + + // Shiro redis 连接信息 + @Value("${spring.redis.shiro.host}") + private String host; + @Value("${spring.redis.shiro.database}") + private int database; + @Value("${spring.redis.shiro.timeout}") + private int timeout; + @Value("${spring.redis.shiro.password}") + private String password; + + /** + * 配置shiro redisManager + */ + public RedisManager redisManager() { + RedisManager redisManager = new RedisManager(); + redisManager.setHost(host); + if(StringUtils.hasText(password)){ + redisManager.setPassword(password); + } + redisManager.setDatabase(database); + redisManager.setTimeout(timeout); + return redisManager; + } + + /** + * cacheManager 缓存 redis 实现 + */ + @Bean + public RedisCacheManager cacheManager() { + RedisCacheManager redisCacheManager = new RedisCacheManager(); + redisCacheManager.setRedisManager(redisManager()); + return redisCacheManager; + } + + /** + * RedisSessionDAO redis 实现 + */ + @Bean + public RedisSessionDAO redisSessionDAO() { + RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); + redisSessionDAO.setRedisManager(redisManager()); + return redisSessionDAO; + } + + // 自定义sessionManager + @Bean + public SessionManager sessionManager() { + MySessionManager mySessionManager = new MySessionManager(); + mySessionManager.setSessionDAO(redisSessionDAO()); + return mySessionManager; + } + +} +``` + +SysUser 实体类要实现 Serializable 接口 +``` java +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SysUser implements Serializable { + // ... +} +``` + +其它代码照旧 + + + + +(结合上部分,同时做到集成 Redis + 前后端分离) + +1、`pom.xml` 引入依赖 +``` xml + + + org.springframework.session + spring-session-data-redis + + + org.springframework.boot + spring-boot-starter-data-redis + +``` + +2、`yml` 增加配置 +``` yml +spring: + session: + store-type: redis + timeout: 8H + redis: + namespace: spring:session + + data: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 3 + # 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 +``` + +3、在 `CustomAccessDeniedHandler` 自定义认证异常处理类中,返回 `json` 格式数据 + +``` java +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler, AuthenticationEntryPoint, Serializable { + + // 未登录异常 + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + //验证为未登陆状态会进入此方法,认证错误 + response.setStatus(401); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json; charset=utf-8"); + PrintWriter printWriter = response.getWriter(); + String body = new ObjectMapper().writeValueAsString(AjaxJson.get(401, "请先进行登录")); + printWriter.write(body); + printWriter.flush(); + } + + // 权限不足 + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { + // 登陆状态下,权限不足执行该方法 + response.setStatus(200); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json; charset=utf-8"); + PrintWriter printWriter = response.getWriter(); + String body = new ObjectMapper().writeValueAsString(AjaxJson.get(403, "权限不足")); + printWriter.write(body); + printWriter.flush(); + } + +} +``` + +4、别忘了注入到 `SecurityFilterChain` 过滤器链 +``` java + // 异常处理 +httpSecurity.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> { + // 权限不足处理方案 + httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(accessDeniedHandler); + // 未登录 处理逻辑 + httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(accessDeniedHandler); +}); +``` + +5、在登录时,返回对应 token 信息 +``` java +// 测试登录 +@RequestMapping("doLogin") +public AjaxJson doLogin(String username, String password, HttpServletRequest request) { + try { + // 验证账号密码 + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password); + usernamePasswordAuthenticationToken.setDetails(sysUserDao.findByUsername(username)); + Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken); + // 存入上下文 + SecurityContextHolder.getContext().setAuthentication(authentication); + request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext()); + // 返回 + String token = request.getSession().getId(); + return AjaxJson.getSuccess("登录成功!").set("token", token); + } catch (Exception e) { + e.printStackTrace(); + return AjaxJson.getError(e.getMessage()); + } +} +``` + +6、前端改造 +- 1、在登录请求时,将返回的 token 保存到本地 `localStorage.setItem('token', res.token)`。 +- 2、在后续每次请求中,读取本地保存的 token 塞到请求 header 中 + +``` js +const header = {}; +if(localStorage.token) { + header.token = localStorage.token; +} +// 后续提交请求... +``` + +7、新建 `HttpSessionConfigure` 配置重写 `HttpSessionId` 读取策略,改为从 `header` 头读取 `token` 参数作为 `SessionId` +``` java +@Configuration +public class HttpSessionConfigure { + // HttpSession 读取策略,从 header 头读取 token 参数作为 session id + @Bean + public HeaderHttpSessionIdResolver httpSessionStrategy() { + System.out.println("----------------- 自定义 HttpSession Id 读取方式"); + return new HeaderHttpSessionIdResolver("token"); + } +} +``` + + + +无 + + + + + + + + + + + + + diff --git a/sa-token-doc/fun/curr-domain.md b/sa-token-doc/fun/curr-domain.md index 1a7c21840f405323a967c0f72e9a03e41af9ecd3..33c6a8c930fcbac87fc841f002978883e97b9942 100644 --- a/sa-token-doc/fun/curr-domain.md +++ b/sa-token-doc/fun/curr-domain.md @@ -58,7 +58,8 @@ public class CustomSaTokenContextForSpring extends SaTokenContextForSpring { 其它逻辑保持不变,框架即可正确获取 uri 地址 -!> 注意:步骤一与步骤二需要同步存在,否则可能有前端假传 header 参数造成安全问题 +> [!ATTENTION| label:风险警告] +> 注意:步骤一与步骤二需要同步存在,否则可能有前端假传 header 参数造成安全问题 ### 方案二:直接在yml中配置当前项目的网络访问地址 diff --git a/sa-token-doc/fun/custom-annotations.md b/sa-token-doc/fun/custom-annotations.md new file mode 100644 index 0000000000000000000000000000000000000000..c992517d3a55101658d47a39eebe15b04a313272 --- /dev/null +++ b/sa-token-doc/fun/custom-annotations.md @@ -0,0 +1,199 @@ +# 自定义注解 + +如果框架内置的注解无法满足你的业务需求,你还可以自定义注解注入到框架中。 + +--- + +### 1、自定义注解 + +假设有以下业务需求 + +> [!INFO| label:需求场景] +> 自定义一个注解 `@CheckAccount`,具有 `name`、`pwd` 两个字段,在标注一个方法上时,要求前端必须提交相应的账号密码参数才能访问方法。 + + +#### 1.1、第一步,创建一个注解 + +``` java +/** + * 账号校验:在标注一个方法上时,要求前端必须提交相应的账号密码参数才能访问方法。 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE}) +public @interface CheckAccount { + + /** + * 需要校验的账号 + */ + String name(); + + /** + * 需要校验的密码 + */ + String pwd(); + +} +``` + +#### 1.2、第二步,创建注解处理器 + +实现 `SaAnnotationAbstractHandler` 接口,指定泛型为刚才自定义的注解 + +``` java +/** + * 注解 CheckAccount 的处理器 + */ +@Component +public class CheckAccountHandler implements SaAnnotationAbstractHandler { + + // 指定这个处理器要处理哪个注解 + @Override + public Class getHandlerAnnotationClass() { + return CheckAccount.class; + } + + // 每次请求校验注解时,会执行的方法 + @Override + public void checkMethod(CheckAccount at, AnnotatedElement element) { + // 获取前端请求提交的参数 + String name = SaHolder.getRequest().getParamNotNull("name"); + String pwd = SaHolder.getRequest().getParamNotNull("pwd"); + + // 与注解中指定的值相比较 + if(name.equals(at.name()) && pwd.equals(at.pwd()) ) { + // 校验通过,什么也不做 + } else { + // 校验不通过,则抛出异常 + throw new SaTokenException("账号或密码错误,未通过校验"); + } + } + +} +``` + +参考上述代码,实现类上指定了 `@Component` 注解,使其可以在 ioc 环境下(如 Spring)被自动扫描注册 Sa-Token 中, +如果你的项目属于非 ioc 环境,则需要手动将其注册到 Sa-Token 框架中: +``` java +SaAnnotationStrategy.instance.registerAnnotationHandler(new CheckAccountHandler()); +``` + +#### 1.3、测试自定义的注解 + +我们在一个请求接口上指定这个注解,来测试一下效果 + +``` java +@RestController +@RequestMapping("/test/") +public class TestController { + + @RequestMapping("test") + @CheckAccount(name = "sa", pwd = "123456") + public SaResult test() { + System.out.println("------------进来了"); + return SaResult.ok(); + } + +} +``` + +启动项目,使用浏览器访问此接口。 + +先来个错误的账号密码访问测试一下:[http://localhost:8081/test/test?name=sa&pwd=123](http://localhost:8081/test/test?name=sa&pwd=123) + +返回结果: + +``` js +{ + "code": 500, + "msg": "账号或密码错误,未通过校验", + "data": null +} +``` + +使用正确账号密码测试访问:[http://localhost:8081/test/test?name=sa&pwd=123456](http://localhost:8081/test/test?name=sa&pwd=123456) + +返回结果: + +``` js +{ + "code": 200, + "msg": "ok", + "data": null +} +``` + + + +### 2、使用自定义注解优化多账号鉴权 + +在之前的 [ 多账号鉴权 ] 章节,我们介绍了利用 “spring 注解处理器” 达到注解合并的目的,从而简化多账号体系下的注解鉴权写法。 + +此种方案比较简单,但是也有一些缺点。 +- 1、强依赖 Spring,无法在非 Spring 环境中使用。 +- 2、注解递归检查可能会造成一些性能下降。 +- 3、扩展性较低,只能略微简化框架内置好的注解写法,无法灵活扩展功能。 + +此处我们再演示一种方案,使用自定义注解的方式达到相同的目的。 + + +#### 2.1、首先定义注解 + +``` java +/** + * 登录认证(User版):只有登录之后才能进入该方法 + *

可标注在函数、类上(效果等同于标注在此类的所有方法上) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE}) +public @interface SaUserCheckLogin { + +} +``` + +#### 2.2、定义注解处理器 +``` java +/** + * 注解 SaUserCheckLogin 的处理器 + */ +@Component +public class SaUserCheckLoginHandler implements SaAnnotationAbstractHandler { + + @Override + public Class getHandlerAnnotationClass() { + return SaUserCheckLogin.class; + } + + @Override + public void checkMethod(SaUserCheckLogin at, AnnotatedElement element) { + SaCheckLoginHandler._checkMethod(StpUserUtil.TYPE); + } + +} +``` + +#### 2.3、使用新注解 +接下来就可以使用我们的自定义注解了: + +``` java +// 使用 @SaUserCheckLogin 的效果等同于使用:@SaCheckLogin(type = "user") +@SaUserCheckLogin +@RequestMapping("info") +public String info() { + return "查询用户信息"; +} +``` + +注:其它注解 `@SaCheckRole("xxx")`、`@SaCheckPermission("xxx")` 同理, 完整示例参考 Gitee 代码: +[自定义注解](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation)。 + + + + + + + + + + + + diff --git a/sa-token-doc/fun/data-structure.md b/sa-token-doc/fun/data-structure.md new file mode 100644 index 0000000000000000000000000000000000000000..f5595551fee1ed11cdb9f1c204d93332af3a58c7 --- /dev/null +++ b/sa-token-doc/fun/data-structure.md @@ -0,0 +1,362 @@ +# 数据结构 + + +## 1、登录会话 + +### 1.1、token -> loginId 映射 + +``` js +{tokenName}:{loginType}:token:{tokenValue} +``` + +

+详细 + +key 示例 (ttl 为 timeout 有效期值 ) +``` js +satoken:login:token:47ab0105-2be1-400c-b517-82f81a0cfcf8 +``` + + +正常 value 格式 +``` js +10001 loginId,登录id,一般为账号id +``` + +异常 value 格式 +``` js +-1 未能从请求中读取到有效 token +-2 已读取到 token,但是 token 无效 +-3 已读取到 token,但是 token 已经过期 (详) +-4 已读取到 token,但是 token 已被顶下线 +-5 已读取到 token,但是 token 已被踢下线 +-6 已读取到 token,但是 token 已被冻结 +-7 未按照指定前缀提交 token +``` + +
+ + +### 1.2、active-timeout + +``` js +{tokenName}:{loginType}:last-active:{tokenValue} +``` + +
+详细 + +key 示例 (key 的 ttl 为 timeout 有效期值 ) +``` js +satoken:login:last-active:06d1f12b-614e-4c00-8d8e-c07fef5f4aa9 +``` + +value 格式 +``` +1722334954193 // 单值时:此 token 最后访问日期 +1722334954193, 1200 // 双值时:此 token 最后访问日期,此 token 指定的动态 active-timeout 值 +``` + +active-timeout 判断方式: +``` js +当前时间 - token 最后访问时间 > active-timeout +返回 true: 此 token 已冻结 +返回 false:此 token 未冻结 +``` + +
+ + + +### 1.3、SaSession + +``` js +{tokenName}:{loginType}:session:{loginId} // Account-Session +{tokenName}:{loginType}:token-session:{loginId} // Token-Session +{tokenName}:custom:session:{sessionId} // Custom-Session +``` + +
+详细 + +key 示例 +``` js +// Account-Session +satoken:login:session:1000001 + +// Token-Session +satoken:login:session:47ab0105-2be1-400c-b517-82f81a0cfcf8 + +// Custom-Session +satoken:custom:session:role-1001 +``` + +value 格式 + +``` js +{ + "@class": "cn.dev33.satoken.dao.SaSessionForJacksonCustomized", // java calss 信息 + "id": "satoken:login:session:10001", // sessionId + "type": "Account-Session", // session类型:Account-Session / Token-Session / Custom-Session + "loginType": "login", // 账号类型 + "loginId": [ // 对应登录id 值(Account-Session才会有值) + "java.lang.Long", + 10001 + ], + "token": null, // 对应 token 值 (Token-Session才会有值) + "createTime": 1722334954145, // 此 session 创建时间,13位时间戳 + "dataMap": { // 此 session 挂载数据 + "@class": "java.util.concurrent.ConcurrentHashMap", + "name": "张三" // 此 session 挂载数据 详情 + // 更多值 ... + }, + "tokenSignList": [ // 客户端 token 信息列表(Account-Session才会有值) + "java.util.Vector", + [ + { + "@class": "cn.dev33.satoken.session.TokenSign", + "value": "06d1f12b-614e-4c00-8d8e-c07fef5f4aa9", // 客户端 token 值 + "device": "default-device", // 登录设备 + "tag": null // 挂载自定义值 + } + ] + ] +} +``` + +
+ + +### 1.4、二级认证 +``` js +{tokenName}:{loginType}:safe:{service}:{tokenValue} +``` +value 为常亮值:`SAFE_AUTH_SAVE_VALUE` + + +### 1.5、账号服务封禁 +``` js +{tokenName}:{loginType}:disable:{service}:{loginId} +``` +value 为封禁等级,int类型 + + +### 1.6、其它 +SaApplication 全局变量 +``` js +{tokenName}:var:{变量名} +``` + +本次请求新创建 token,存储 key +``` js +JUST_CREATED_ +``` + +本次请求新创建 token,存储 key (无前缀方式) +``` js +JUST_CREATED_NOT_PREFIX_ +``` + +临时身份切换,使用的key +``` js +SWITCH_TO_SAVE_KEY_{loginType} +``` + + +## 2、SSO 单点登录 + +### 2.1、ticket -> loginId 映射 +``` js +{tokenName}:ticket:{ticket} +``` +值为 loginId + + +### 2.2、ticket -> client 映射 +``` js +{tokenName}:ticket-client:{ticket} +``` +值为 client + + +### 2.3、loginId -> ticket 映射(loginId 反查 ticket) +``` js +{tokenName}:id-ticket:{id} +``` +值为 ticket + + + +## 3、OAuth2 统一认证 + +### 3.1、Code 授权码 +``` js +{tokenName}:oauth2:code:{code} +``` + +
+详细 + +值为 CodeModel + +``` js +{ + "@class": "cn.dev33.satoken.oauth2.model.CodeModel", // java class 信息 + "code": "AbRVp2HrgyklE0BXYWszskGJWAGY7xhGu6Zaco4zJECzGYagCCFWj0jOlHza", // code值 + "scope": "", // 所申请权限列表,多个用逗号隔开 + "loginId": "10001", // 对应的loginId + "redirectUri": "", // 重定向地址 +} +``` + +
+ +clientId + loginId 反查 code +``` js +{tokenName}:oauth2:code-index:{clientId}:{loginId} +``` + + + +### 3.2、Access-Token 资源令牌 +``` js +{tokenName}:oauth2:access-token:{accessToken} +``` + +
+详细 + +值为 AccessTokenModel + +``` js +{ + "@class": "cn.dev33.satoken.oauth2.model.AccessTokenModel", // java class 信息 + "accessToken": "CqRVp2HrgyklE0BXYWszskGJWAGY7xhGu9Zaco4zJECzGYagCCFWj0jOlHoU", // 资源令牌值 + "refreshToken": "EAubykIqRLwbvvi0wfZqnWxoC1bLhPguIfTqX3S1aoTe6pCLKsV9jU3OEI8U", // 刷新令牌值 + "expiresTime": 1722422031510, // 资源令牌到期时间 + "refreshExpiresTime": 1725006831511, // 刷新令牌到期时间 + "clientId": "1001", // 对应的应用id + "loginId": "10001", // 对应的loginId + "openid": "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__", // 对应的 openid + "scope": "", // 所具有的权限列表,多个用逗号隔开 + "expiresIn": 7199, // 资源令牌剩余有效时间,单位秒 + "refreshExpiresIn": 2592000 // 刷新令牌剩余有效时间,单位秒 +} +``` + +
+ +clientId + loginId 反查 Access-Token +``` js +{tokenName}:oauth2:access-token-index:{clientId}:{loginId} +``` + + +### 3.3、Refresh-Token 资源令牌 +``` js +{tokenName}:oauth2:refresh-token:{refreshToken} +``` + +
+详细 + +值为 RefreshTokenModel + +``` js +{ + "@class": "cn.dev33.satoken.oauth2.model.RefreshTokenModel", // java class 信息 + "refreshToken": "EAubykIqRLwbvvi0wfZqnWxoC1bLhPguIfTqX3S1aoTe6pCLKsV9jU3OEI8U", // 刷新令牌值 + "expiresTime": 1725006831511, // 刷新令牌到期时间 + "clientId": "1001", // 对应的应用id + "scope": "", // 所具有的权限列表,多个用逗号隔开 + "loginId": "10001", // 对应的loginId + "openid": "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__", // 对应的 openid + "expiresIn": 2591999 // 刷新令牌剩余有效时间,单位秒 +} +``` + +
+ +clientId + loginId 反查 Refresh-Token +``` js +{tokenName}:oauth2:refresh-token-index:{clientId}:{loginId} +``` + + +### 3.4、Client-Token 应用令牌 +``` js +{tokenName}:oauth2:client-token:{clientToken} +``` + +
+详细 + +值为 ClientTokenModel + +``` js +{ + "@class": "cn.dev33.satoken.oauth2.model.ClientTokenModel", // java class 信息 + "clientToken": "fWQjBKxprSslmYFLbzen0oa95rOvqnqYKZW3sD8mzamNbabG8b6MPKPP5uCu", // 应用令牌值 + "expiresTime": 1722425237153, // 应用令牌到期时间 + "clientId": "1001", // 对应的应用id + "scope": null, // 所具有的权限列表,多个用逗号隔开 + "expiresIn": 7200 // 应用令牌剩余有效时间,单位秒 +} +``` + +
+ +clientId 反查 Client-Token +``` js +{tokenName}:oauth2:client-token-index:{clientId} +``` + +Lower-Client-Token 次级应用令牌索引 +``` js +{tokenName}:oauth2:lower-client-token-index:{clientId} +``` + +### 3.5、用户授权记录 +``` js +{tokenName}:oauth2:grant-scope:{clientId}:{loginId} +``` +值为 scope 列表,多个用逗号隔开 + + + + +## 4、插件 + +### 4.1、临时 token 会话 +``` js +{tokenName}:temp-token:{service}:{token} +``` + + +### 4.2、 Same-Token + +Same-Token +``` js +{tokenName}:var:same-token +``` + +Past-Same-Token +``` js +{tokenName}:var:past-same-token +``` + + +### 4.3、Sign 签名 + +随机字符串 +``` js +{tokenName}:sign:nonce:{32位随机字符} +``` + + + + + + + + diff --git a/sa-token-doc/fun/exception-code.md b/sa-token-doc/fun/exception-code.md index 0675dc1f2bdbb99d0894fab4b8942428277573ac..b2df661e4c1d784b457234e46743ceda5e4a6bbd 100644 --- a/sa-token-doc/fun/exception-code.md +++ b/sa-token-doc/fun/exception-code.md @@ -53,8 +53,6 @@ SaToken 中的所有异常都是继承于 `SaTokenException` 的,也就是说 ### 异常细分状态码-参照表 -!> 部分插件因异常抛出点较少,暂未做状态码细分处理 - #### sa-token-code 核心包 | code码值 | 含义 | @@ -162,37 +160,32 @@ SaToken 中的所有异常都是继承于 `SaTokenException` 的,也就是说 #### sa-token-oauth2 相关: -| code码值 | 含义 | -| :-------- | :-------- | -| 30101 | client_id 不可为空 | -| 30102 | scope 不可为空 | +| code码值 | 含义 | +| :-------- | :-------- | +| 30101 | client_id 不可为空 | +| 30102 | scope 不可为空 | | 30103 | redirect_uri 不可为空 | -| 30104 | LoginId 不可为空 | -| 30105 | 无效client_id | -| 30106 | 无效access_token | -| 30107 | 无效 client_token | -| 30108 | Access-Token 不具备指定的 Scope | -| 30109 | Client-Token 不具备指定的 Scope | -| 30110 | 无效 code 码 | -| 30111 | 无效 Refresh-Token | -| 30112 | 请求的Scope暂未签约 | -| 30113 | 无效redirect_url | -| 30114 | 非法redirect_url | -| 30115 | 无效client_secret | -| 30116 | 请求的Scope暂未签约 | -| 30117 | 无效code | -| 30118 | 无效client_id | -| 30119 | 无效client_secret | -| 30120 | 无效redirect_uri | -| 30121 | 无效refresh_token | -| 30122 | 无效client_id | -| 30123 | 无效client_secret | -| 30124 | 无效client_id | -| 30125 | 无效response_type | -| 30131 | 暂未开放授权码模式 | -| 30132 | 暂未开放隐藏式模式 | -| 30133 | 暂未开放密码式模式 | -| 30134 | 暂未开放凭证式模式 | +| 30104 | LoginId 不可为空 | +| 30105 | 无效 client_id | +| 30106 | 无效 access_token | +| 30107 | 无效 client_token | +| 30108 | Access-Token 不具备指定的 Scope | +| 30109 | Client-Token 不具备指定的 Scope | +| 30110 | 无效 code 码 | +| 30111 | 无效 Refresh-Token | +| 30112 | 请求的 Scope 暂未签约 | +| 30113 | 无效 redirect_url | +| 30114 | 非法 redirect_url | +| 30115 | 无效 client_secret | +| 30120 | redirect_uri 不一致 | +| 30122 | client_id 不一致 | +| 30125 | 无效 response_type | +| 30126 | 无效 grant_type | +| 30127 | 无效 state | +| 30141 | 系统暂未开放的授权模式 | +| 30142 | 应用暂未开放的授权模式 | +| 30151 | 无效的请求 Method | +| 30191 | 其它异常 | #### sa-token-jwt 插件相关: @@ -215,6 +208,9 @@ SaToken 中的所有异常都是继承于 `SaTokenException` 的,也就是说 | 30303 | Token已超时 | +> [!WARNING| label:注意] +> 部分插件因异常抛出点较少,暂未做状态码细分处理 + diff --git a/sa-token-doc/fun/issue-template.md b/sa-token-doc/fun/issue-template.md index ccd2de20cc846cf3172437a81756bb037a5476dd..f703028b5784520692b5fcb1100d2da404545f0f 100644 --- a/sa-token-doc/fun/issue-template.md +++ b/sa-token-doc/fun/issue-template.md @@ -2,8 +2,7 @@ 在线提问链接:[Gitee issue](https://gitee.com/dromara/sa-token/issues)、[GitHub issue](https://github.com/dromara/sa-token/issues) -> 请在新建 issue 时,尽量复制模板格式进行提交 -> +> [!TIP| label:请在新建 issue 时,尽量复制模板格式进行提交] > 1. 提交之前率先参考 Sa-Token 常见问题解答 以及善用 Gitee issues 搜索功能,查阅问题是否已有答案,已存在的 issue 就不要再重复提交了。 > 2. 问题已得到处理的 issue 请大家及时手动关闭,如果超过24小时没有追问,我们将默认提交者已找到解决方案,关闭issue。 > 3. 有时候 issue 提交之后,没有得到及时回复,大家可以加入QQ群@管理员寻求帮助。 diff --git a/sa-token-doc/fun/jur-cache.md b/sa-token-doc/fun/jur-cache.md index b1c1d7657bc4c4692049dc10483788df97e00135..6f12ad232b4254b4a9ca31415e42b0968773e0ec 100644 --- a/sa-token-doc/fun/jur-cache.md +++ b/sa-token-doc/fun/jur-cache.md @@ -11,34 +11,43 @@ @Component public class StpInterfaceImpl implements StpInterface { - // 返回一个账号所拥有的权限码集合 - @Override - public List getPermissionList(Object loginId, String loginType) { - - // 1. 声明权限码集合 - List permissionList = new ArrayList<>(); - - // 2. 遍历角色列表,查询拥有的权限码 - for (String roleId : getRoleList(loginId, loginType)) { - SaSession roleSession = SaSessionCustomUtil.getSessionById("role-" + roleId); - List list = roleSession.get("Permission_List", () -> { - return ...; // 从数据库查询这个角色所拥有的权限列表 - }); - permissionList.addAll(list); - } - - // 3. 返回权限码集合 - return permissionList; - } - - // 返回一个账号所拥有的角色标识集合 - @Override - public List getRoleList(Object loginId, String loginType) { - SaSession session = StpUtil.getSessionByLoginId(loginId); - return session.get("Role_List", () -> { - return ...; // 从数据库查询这个账号id拥有的角色列表 - }); - } + // 返回一个账号所拥有的权限码集合 + @Override + @SuppressWarnings("unchecked") + public List getPermissionList(Object loginId, String loginType) { + + // 1. 声明权限码集合 + List list = new ArrayList<>(); + + // 2. 遍历角色列表,查询拥有的权限码 + for (String roleId : getRoleList(loginId, loginType)) { + List permissionList = (List)SaManager.getSaTokenDao().getObject("satoken:role-find-permission:" + roleId); + if(permissionList == null) { + // 从数据库查询这个角色 id 所拥有的权限列表 + permissionList = ... + // 查好后,set 到缓存中 + SaManager.getSaTokenDao().setObject("satoken:role-find-permission:" + roleId, permissionList, 60 * 60 * 24 * 30); + } + list.addAll(permissionList); + } + + // 3. 返回权限码集合 + return list; + } + + // 返回一个账号所拥有的角色标识集合 + @Override + @SuppressWarnings("unchecked") + public List getRoleList(Object loginId, String loginType) { + List roleList = (List)SaManager.getSaTokenDao().getObject("satoken:loginId-find-role:" + loginId); + if(roleList == null) { + // 从数据库查询这个账号id拥有的角色列表, + roleList = ... + // 查好后,set 到缓存中 + SaManager.getSaTokenDao().setObject("satoken:loginId-find-role:" + loginId, roleList, 60 * 60 * 24 * 30); + } + return roleList; + } } ``` diff --git a/sa-token-doc/fun/plugin-dev.md b/sa-token-doc/fun/plugin-dev.md index 07eea45079187afc5bbf795c13ecd5d648b12453..944ba1c82047d6fa16cc408ab4e4cac06d0e99b0 100644 --- a/sa-token-doc/fun/plugin-dev.md +++ b/sa-token-doc/fun/plugin-dev.md @@ -124,7 +124,7 @@ SaTokenContext 是对接不同框架的上下文接口,注入流程和第二 ### 6、发布代码 -插件开发完毕之后,你可以将其pr到官方仓库,或: +插件开发完毕之后,你可以将其 pr 到 [sa-token-three-plugin](https://gitee.com/sa-tokens/sa-token-three-plugin),或: 上传到 gitee/github 作为独立项目维护,并发布到 Maven 中央仓库,参考这篇:[https://juejin.cn/post/6844904104834105358](https://juejin.cn/post/6844904104834105358) diff --git a/sa-token-doc/fun/session-model.md b/sa-token-doc/fun/session-model.md index 496ab6a414e7f5ba12eac47be24c172a38ffdfbe..a10f40625b366b4b9c44051975232bb4aa0ac912 100644 --- a/sa-token-doc/fun/session-model.md +++ b/sa-token-doc/fun/session-model.md @@ -42,6 +42,7 @@ session.set("name", "张三"); 随着业务推进,我们还可能会遇到一些需要数据隔离的场景: +> [!NOTE| label:业务场景] > 指定客户端超过两小时无操作就自动下线,如果两小时内有操作,就再续期两小时,直到新的两小时无操作 那么这种请求访问记录应该存储在哪里呢?放在 Account-Session 里吗? diff --git a/sa-token-doc/fun/sso-vs-oauth2.md b/sa-token-doc/fun/sso-vs-oauth2.md index a3cd5764d26543864455bbe68660f15ad7ad74a0..e8dd8b0f429664ca15caca7221b379cbd7b328ee 100644 --- a/sa-token-doc/fun/sso-vs-oauth2.md +++ b/sa-token-doc/fun/sso-vs-oauth2.md @@ -14,6 +14,7 @@ QQ群库经常有小伙伴提问:项目需要搭建统一认证中心,是用 | 自有系统授权管理 | 支持度高 | 支持度低 | | Client级的权限校验 | 不支持 | 支持度高 | | 集成简易度 | 比较简单 | 难度中等 | +| 适合项目 | 企业内部项目整合 | 企业搭建统一认证授权平台,对外开放服务 | 注:以上仅为在 Sa-Token 中两种技术的差异度比较,不同框架的实现可能略有差异,但整体思想是一致的。 diff --git a/sa-token-doc/fun/token-timeout.md b/sa-token-doc/fun/token-timeout.md index 309110dd26656ae84f1456b682da36246cc81f1a..978292be8686ea9e212bba8e0acdb60579c881ee 100644 --- a/sa-token-doc/fun/token-timeout.md +++ b/sa-token-doc/fun/token-timeout.md @@ -25,6 +25,7 @@ sa-token.active-timeout=-1 两者的区别,可以通过下面的例子体现: +> [!TIP| label:场景示例] > 1. 假设你到银行要存钱,首先就要办理一张卡 (要访问系统接口先登录)。 > 2. 银行为你颁发一张储蓄卡(系统为你颁发一个Token),以后每次存取钱都要带上这张卡(后续每次访问系统都要提交 Token)。 > 3. 银行为这张卡设定两个过期时间: diff --git a/sa-token-doc/index.html b/sa-token-doc/index.html index a7abea2ce563c30a396bc67cb2d60d5a78b80996..facffd84b5821ea86d18bed08254f34ddfcf8775 100644 --- a/sa-token-doc/index.html +++ b/sa-token-doc/index.html @@ -31,11 +31,9 @@